mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Merge branch 'dev' into esphome_bronze
This commit is contained in:
commit
3c4f540ce0
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.11"]
|
"requirements": ["aioairzone-cloud==0.6.12"]
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"off_grid_status": {
|
"off_grid_status": {
|
||||||
"name": "Off grid status"
|
"name": "Off-grid status"
|
||||||
},
|
},
|
||||||
"dc_1_short_circuit_error_status": {
|
"dc_1_short_circuit_error_status": {
|
||||||
"name": "DC 1 short circuit error status"
|
"name": "DC 1 short circuit error status"
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"threshold": {
|
"threshold": {
|
||||||
"state": {
|
"state": {
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"green": "Green",
|
"green": "Green",
|
||||||
"yellow": "Yellow",
|
"yellow": "Yellow",
|
||||||
"red": "Red"
|
"red": "Red"
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"unavailable": "Unavailable",
|
"unavailable": "Unavailable",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"offline": "Offline"
|
"offline": "Offline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"vehicle_detected": "Detected",
|
"vehicle_detected": "Detected",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"no_power": "No power",
|
"no_power": "No power",
|
||||||
"vehicle_error": "Error"
|
"vehicle_error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actual_v1": {
|
"actual_v1": {
|
||||||
|
@ -139,7 +139,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"complete": "Complete",
|
"complete": "Complete",
|
||||||
"fully_charged": "Fully charged",
|
"fully_charged": "Fully charged",
|
||||||
"finished_fully_charged": "Finished, fully charged",
|
"finished_fully_charged": "Finished, fully charged",
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from fnmatch import translate
|
from fnmatch import translate
|
||||||
from functools import lru_cache, partial
|
from functools import lru_cache, partial
|
||||||
@ -66,13 +65,12 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo as _DhcpServ
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import websocket_api
|
||||||
|
from .const import DOMAIN, HOSTNAME, IP_ADDRESS, MAC_ADDRESS
|
||||||
|
from .models import DATA_DHCP, DHCPAddressData, DHCPData, DhcpMatchers
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
HOSTNAME: Final = "hostname"
|
|
||||||
MAC_ADDRESS: Final = "macaddress"
|
|
||||||
IP_ADDRESS: Final = "ip"
|
|
||||||
REGISTERED_DEVICES: Final = "registered_devices"
|
REGISTERED_DEVICES: Final = "registered_devices"
|
||||||
SCAN_INTERVAL = timedelta(minutes=60)
|
SCAN_INTERVAL = timedelta(minutes=60)
|
||||||
|
|
||||||
@ -87,15 +85,6 @@ _DEPRECATED_DhcpServiceInfo = DeprecatedConstant(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class DhcpMatchers:
|
|
||||||
"""Prepared info from dhcp entries."""
|
|
||||||
|
|
||||||
registered_devices_domains: set[str]
|
|
||||||
no_oui_matchers: dict[str, list[DHCPMatcher]]
|
|
||||||
oui_matchers: dict[str, list[DHCPMatcher]]
|
|
||||||
|
|
||||||
|
|
||||||
def async_index_integration_matchers(
|
def async_index_integration_matchers(
|
||||||
integration_matchers: list[DHCPMatcher],
|
integration_matchers: list[DHCPMatcher],
|
||||||
) -> DhcpMatchers:
|
) -> DhcpMatchers:
|
||||||
@ -133,36 +122,34 @@ def async_index_integration_matchers(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the dhcp component."""
|
"""Set up the dhcp component."""
|
||||||
watchers: list[WatcherBase] = []
|
|
||||||
address_data: dict[str, dict[str, str]] = {}
|
|
||||||
integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass))
|
integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass))
|
||||||
|
dhcp_data = DHCPData(integration_matchers=integration_matchers)
|
||||||
|
hass.data[DATA_DHCP] = dhcp_data
|
||||||
|
websocket_api.async_setup(hass)
|
||||||
|
watchers: list[WatcherBase] = []
|
||||||
# For the passive classes we need to start listening
|
# For the passive classes we need to start listening
|
||||||
# for state changes and connect the dispatchers before
|
# for state changes and connect the dispatchers before
|
||||||
# everything else starts up or we will miss events
|
# everything else starts up or we will miss events
|
||||||
device_watcher = DeviceTrackerWatcher(hass, address_data, integration_matchers)
|
device_watcher = DeviceTrackerWatcher(hass, dhcp_data)
|
||||||
device_watcher.async_start()
|
device_watcher.async_start()
|
||||||
watchers.append(device_watcher)
|
watchers.append(device_watcher)
|
||||||
|
|
||||||
device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher(
|
device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher(hass, dhcp_data)
|
||||||
hass, address_data, integration_matchers
|
|
||||||
)
|
|
||||||
device_tracker_registered_watcher.async_start()
|
device_tracker_registered_watcher.async_start()
|
||||||
watchers.append(device_tracker_registered_watcher)
|
watchers.append(device_tracker_registered_watcher)
|
||||||
|
|
||||||
async def _async_initialize(event: Event) -> None:
|
async def _async_initialize(event: Event) -> None:
|
||||||
await aiodhcpwatcher.async_init()
|
await aiodhcpwatcher.async_init()
|
||||||
|
|
||||||
network_watcher = NetworkWatcher(hass, address_data, integration_matchers)
|
network_watcher = NetworkWatcher(hass, dhcp_data)
|
||||||
network_watcher.async_start()
|
network_watcher.async_start()
|
||||||
watchers.append(network_watcher)
|
watchers.append(network_watcher)
|
||||||
|
|
||||||
dhcp_watcher = DHCPWatcher(hass, address_data, integration_matchers)
|
dhcp_watcher = DHCPWatcher(hass, dhcp_data)
|
||||||
await dhcp_watcher.async_start()
|
await dhcp_watcher.async_start()
|
||||||
watchers.append(dhcp_watcher)
|
watchers.append(dhcp_watcher)
|
||||||
|
|
||||||
rediscovery_watcher = RediscoveryWatcher(
|
rediscovery_watcher = RediscoveryWatcher(hass, dhcp_data)
|
||||||
hass, address_data, integration_matchers
|
|
||||||
)
|
|
||||||
rediscovery_watcher.async_start()
|
rediscovery_watcher.async_start()
|
||||||
watchers.append(rediscovery_watcher)
|
watchers.append(rediscovery_watcher)
|
||||||
|
|
||||||
@ -180,18 +167,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
class WatcherBase:
|
class WatcherBase:
|
||||||
"""Base class for dhcp and device tracker watching."""
|
"""Base class for dhcp and device tracker watching."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, dhcp_data: DHCPData) -> None:
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
address_data: dict[str, dict[str, str]],
|
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._integration_matchers = integration_matchers
|
self._callbacks = dhcp_data.callbacks
|
||||||
self._address_data = address_data
|
self._integration_matchers = dhcp_data.integration_matchers
|
||||||
|
self._address_data = dhcp_data.address_data
|
||||||
self._unsub: Callable[[], None] | None = None
|
self._unsub: Callable[[], None] | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -230,18 +212,18 @@ class WatcherBase:
|
|||||||
mac_address = formatted_mac.replace(":", "")
|
mac_address = formatted_mac.replace(":", "")
|
||||||
compressed_ip_address = made_ip_address.compressed
|
compressed_ip_address = made_ip_address.compressed
|
||||||
|
|
||||||
data = self._address_data.get(mac_address)
|
current_data = self._address_data.get(mac_address)
|
||||||
if (
|
if (
|
||||||
not force
|
not force
|
||||||
and data
|
and current_data
|
||||||
and data[IP_ADDRESS] == compressed_ip_address
|
and current_data[IP_ADDRESS] == compressed_ip_address
|
||||||
and data[HOSTNAME].startswith(hostname)
|
and current_data[HOSTNAME].startswith(hostname)
|
||||||
):
|
):
|
||||||
# If the address data is the same no need
|
# If the address data is the same no need
|
||||||
# to process it
|
# to process it
|
||||||
return
|
return
|
||||||
|
|
||||||
data = {IP_ADDRESS: compressed_ip_address, HOSTNAME: hostname}
|
data: DHCPAddressData = {IP_ADDRESS: compressed_ip_address, HOSTNAME: hostname}
|
||||||
self._address_data[mac_address] = data
|
self._address_data[mac_address] = data
|
||||||
|
|
||||||
lowercase_hostname = hostname.lower()
|
lowercase_hostname = hostname.lower()
|
||||||
@ -287,9 +269,19 @@ class WatcherBase:
|
|||||||
_LOGGER.debug("Matched %s against %s", data, matcher)
|
_LOGGER.debug("Matched %s against %s", data, matcher)
|
||||||
matched_domains.add(domain)
|
matched_domains.add(domain)
|
||||||
|
|
||||||
if not matched_domains:
|
if self._callbacks:
|
||||||
return # avoid creating DiscoveryKey if there are no matches
|
address_data = {mac_address: data}
|
||||||
|
for callback_ in self._callbacks:
|
||||||
|
callback_(address_data)
|
||||||
|
|
||||||
|
service_info: _DhcpServiceInfo | None = None
|
||||||
|
if not matched_domains:
|
||||||
|
return
|
||||||
|
service_info = _DhcpServiceInfo(
|
||||||
|
ip=ip_address,
|
||||||
|
hostname=lowercase_hostname,
|
||||||
|
macaddress=mac_address,
|
||||||
|
)
|
||||||
discovery_key = DiscoveryKey(
|
discovery_key = DiscoveryKey(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
key=mac_address,
|
key=mac_address,
|
||||||
@ -300,11 +292,7 @@ class WatcherBase:
|
|||||||
self.hass,
|
self.hass,
|
||||||
domain,
|
domain,
|
||||||
{"source": config_entries.SOURCE_DHCP},
|
{"source": config_entries.SOURCE_DHCP},
|
||||||
_DhcpServiceInfo(
|
service_info,
|
||||||
ip=ip_address,
|
|
||||||
hostname=lowercase_hostname,
|
|
||||||
macaddress=mac_address,
|
|
||||||
),
|
|
||||||
discovery_key=discovery_key,
|
discovery_key=discovery_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -315,11 +303,10 @@ class NetworkWatcher(WatcherBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
address_data: dict[str, dict[str, str]],
|
dhcp_data: DHCPData,
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
super().__init__(hass, dhcp_data)
|
||||||
self._discover_hosts: DiscoverHosts | None = None
|
self._discover_hosts: DiscoverHosts | None = None
|
||||||
self._discover_task: asyncio.Task | None = None
|
self._discover_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
"""Constants for the dhcp integration."""
|
"""Constants for the dhcp integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
DOMAIN = "dhcp"
|
DOMAIN = "dhcp"
|
||||||
|
HOSTNAME: Final = "hostname"
|
||||||
|
MAC_ADDRESS: Final = "macaddress"
|
||||||
|
IP_ADDRESS: Final = "ip"
|
||||||
|
37
homeassistant/components/dhcp/helpers.py
Normal file
37
homeassistant/components/dhcp/helpers.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""The dhcp integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
|
||||||
|
from .models import DATA_DHCP, DHCPAddressData
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_register_dhcp_callback_internal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
callback_: Callable[[dict[str, DHCPAddressData]], None],
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Register a dhcp callback.
|
||||||
|
|
||||||
|
For internal use only.
|
||||||
|
This is not intended for use by integrations.
|
||||||
|
"""
|
||||||
|
callbacks = hass.data[DATA_DHCP].callbacks
|
||||||
|
callbacks.add(callback_)
|
||||||
|
return partial(callbacks.remove, callback_)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_address_data_internal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> dict[str, DHCPAddressData]:
|
||||||
|
"""Get the address data.
|
||||||
|
|
||||||
|
For internal use only.
|
||||||
|
This is not intended for use by integrations.
|
||||||
|
"""
|
||||||
|
return hass.data[DATA_DHCP].address_data
|
43
homeassistant/components/dhcp/models.py
Normal file
43
homeassistant/components/dhcp/models.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""The dhcp integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
import dataclasses
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from homeassistant.loader import DHCPMatcher
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DhcpMatchers:
|
||||||
|
"""Prepared info from dhcp entries."""
|
||||||
|
|
||||||
|
registered_devices_domains: set[str]
|
||||||
|
no_oui_matchers: dict[str, list[DHCPMatcher]]
|
||||||
|
oui_matchers: dict[str, list[DHCPMatcher]]
|
||||||
|
|
||||||
|
|
||||||
|
class DHCPAddressData(TypedDict):
|
||||||
|
"""Typed dict for DHCP address data."""
|
||||||
|
|
||||||
|
hostname: str
|
||||||
|
ip: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(slots=True)
|
||||||
|
class DHCPData:
|
||||||
|
"""Data for the dhcp component."""
|
||||||
|
|
||||||
|
integration_matchers: DhcpMatchers
|
||||||
|
callbacks: set[Callable[[dict[str, DHCPAddressData]], None]] = dataclasses.field(
|
||||||
|
default_factory=set
|
||||||
|
)
|
||||||
|
address_data: dict[str, DHCPAddressData] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DHCP: HassKey[DHCPData] = HassKey(DOMAIN)
|
63
homeassistant/components/dhcp/websocket_api.py
Normal file
63
homeassistant/components/dhcp/websocket_api.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""The dhcp integration websocket apis."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.json import json_bytes
|
||||||
|
|
||||||
|
from .const import HOSTNAME, IP_ADDRESS
|
||||||
|
from .helpers import (
|
||||||
|
async_get_address_data_internal,
|
||||||
|
async_register_dhcp_callback_internal,
|
||||||
|
)
|
||||||
|
from .models import DHCPAddressData
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the DHCP websocket API."""
|
||||||
|
websocket_api.async_register_command(hass, ws_subscribe_discovery)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "dhcp/subscribe_discovery",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_subscribe_discovery(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Handle subscribe discovery websocket command."""
|
||||||
|
ws_msg_id: int = msg["id"]
|
||||||
|
|
||||||
|
def _async_send(address_data: dict[str, DHCPAddressData]) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
json_bytes(
|
||||||
|
websocket_api.event_message(
|
||||||
|
ws_msg_id,
|
||||||
|
{
|
||||||
|
"add": [
|
||||||
|
{
|
||||||
|
"mac_address": dr.format_mac(mac_address).upper(),
|
||||||
|
"hostname": data[HOSTNAME],
|
||||||
|
"ip_address": data[IP_ADDRESS],
|
||||||
|
}
|
||||||
|
for mac_address, data in address_data.items()
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
unsub = async_register_dhcp_callback_internal(hass, _async_send)
|
||||||
|
connection.subscriptions[ws_msg_id] = unsub
|
||||||
|
connection.send_message(json_bytes(websocket_api.result_message(ws_msg_id)))
|
||||||
|
_async_send(async_get_address_data_internal(hass))
|
@ -57,6 +57,7 @@ ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||||
|
DEFAULT_NAME = "ESPHome"
|
||||||
|
|
||||||
|
|
||||||
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
@ -117,8 +118,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self._host = entry_data[CONF_HOST]
|
self._host = entry_data[CONF_HOST]
|
||||||
self._port = entry_data[CONF_PORT]
|
self._port = entry_data[CONF_PORT]
|
||||||
self._password = entry_data[CONF_PASSWORD]
|
self._password = entry_data[CONF_PASSWORD]
|
||||||
self._name = self._reauth_entry.title
|
|
||||||
self._device_name = entry_data.get(CONF_DEVICE_NAME)
|
self._device_name = entry_data.get(CONF_DEVICE_NAME)
|
||||||
|
self._name = self._reauth_entry.title
|
||||||
|
|
||||||
# Device without encryption allows fetching device info. We can then check
|
# Device without encryption allows fetching device info. We can then check
|
||||||
# if the device is no longer using a password. If we did try with a password,
|
# if the device is no longer using a password. If we did try with a password,
|
||||||
@ -147,7 +148,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_encryption_removed_confirm",
|
step_id="reauth_encryption_removed_confirm",
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
@ -172,7 +173,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reconfigure(
|
async def async_step_reconfigure(
|
||||||
@ -189,12 +190,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _name(self) -> str:
|
def _name(self) -> str:
|
||||||
return self.__name or "ESPHome"
|
return self.__name or DEFAULT_NAME
|
||||||
|
|
||||||
@_name.setter
|
@_name.setter
|
||||||
def _name(self, value: str) -> None:
|
def _name(self, value: str) -> None:
|
||||||
self.__name = value
|
self.__name = value
|
||||||
self.context["title_placeholders"] = {"name": self._name}
|
self.context["title_placeholders"] = {
|
||||||
|
"name": self._async_get_human_readable_name()
|
||||||
|
}
|
||||||
|
|
||||||
async def _async_try_fetch_device_info(self) -> ConfigFlowResult:
|
async def _async_try_fetch_device_info(self) -> ConfigFlowResult:
|
||||||
"""Try to fetch device info and return any errors."""
|
"""Try to fetch device info and return any errors."""
|
||||||
@ -254,7 +257,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return await self._async_try_fetch_device_info()
|
return await self._async_try_fetch_device_info()
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="discovery_confirm", description_placeholders={"name": self._name}
|
step_id="discovery_confirm",
|
||||||
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
@ -274,8 +278,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
# Hostname is format: livingroom.local.
|
# Hostname is format: livingroom.local.
|
||||||
device_name = discovery_info.hostname.removesuffix(".local.")
|
device_name = discovery_info.hostname.removesuffix(".local.")
|
||||||
|
|
||||||
self._name = discovery_info.properties.get("friendly_name", device_name)
|
|
||||||
self._device_name = device_name
|
self._device_name = device_name
|
||||||
|
self._name = discovery_info.properties.get("friendly_name", device_name)
|
||||||
self._host = discovery_info.host
|
self._host = discovery_info.host
|
||||||
self._port = discovery_info.port
|
self._port = discovery_info.port
|
||||||
self._noise_required = bool(discovery_info.properties.get("api_encryption"))
|
self._noise_required = bool(discovery_info.properties.get("api_encryption"))
|
||||||
@ -306,7 +310,32 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
updates[CONF_HOST] = host
|
updates[CONF_HOST] = host
|
||||||
if port is not None:
|
if port is not None:
|
||||||
updates[CONF_PORT] = port
|
updates[CONF_PORT] = port
|
||||||
self._abort_if_unique_id_configured(updates=updates)
|
self._abort_unique_id_configured_with_details(updates=updates)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _abort_unique_id_configured_with_details(self, updates: dict[str, Any]) -> None:
|
||||||
|
"""Abort if unique_id is already configured with details."""
|
||||||
|
assert self.unique_id is not None
|
||||||
|
if not (
|
||||||
|
conflict_entry := self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
self.handler, self.unique_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
assert conflict_entry.unique_id is not None
|
||||||
|
if updates:
|
||||||
|
error = "already_configured_updates"
|
||||||
|
else:
|
||||||
|
error = "already_configured_detailed"
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates=updates,
|
||||||
|
error=error,
|
||||||
|
description_placeholders={
|
||||||
|
"title": conflict_entry.title,
|
||||||
|
"name": conflict_entry.data.get(CONF_DEVICE_NAME, "unknown"),
|
||||||
|
"mac": format_mac(conflict_entry.unique_id),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_mqtt(
|
async def async_step_mqtt(
|
||||||
self, discovery_info: MqttServiceInfo
|
self, discovery_info: MqttServiceInfo
|
||||||
@ -341,7 +370,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
await self.async_set_unique_id(mac_address)
|
await self.async_set_unique_id(mac_address)
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={CONF_HOST: self._host, CONF_PORT: self._port}
|
updates={CONF_HOST: self._host, CONF_PORT: self._port}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -479,7 +508,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
data=self._reauth_entry.data | self._async_make_config_data(),
|
data=self._reauth_entry.data | self._async_make_config_data(),
|
||||||
)
|
)
|
||||||
assert self._host is not None
|
assert self._host is not None
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
@ -510,7 +539,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if not (
|
if not (
|
||||||
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
|
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
|
||||||
):
|
):
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
@ -568,9 +597,30 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="encryption_key",
|
step_id="encryption_key",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_get_human_readable_name(self) -> str:
|
||||||
|
"""Return a human readable name for the entry."""
|
||||||
|
entry: ConfigEntry | None = None
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
entry = self._reauth_entry
|
||||||
|
elif self.source == SOURCE_RECONFIGURE:
|
||||||
|
entry = self._reconfig_entry
|
||||||
|
friendly_name = self._name
|
||||||
|
device_name = self._device_name
|
||||||
|
if (
|
||||||
|
device_name
|
||||||
|
and friendly_name in (DEFAULT_NAME, device_name)
|
||||||
|
and entry
|
||||||
|
and entry.title != friendly_name
|
||||||
|
):
|
||||||
|
friendly_name = entry.title
|
||||||
|
if not device_name or friendly_name == device_name:
|
||||||
|
return friendly_name
|
||||||
|
return f"{friendly_name} ({device_name})"
|
||||||
|
|
||||||
async def async_step_authenticate(
|
async def async_step_authenticate(
|
||||||
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -589,7 +639,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="authenticate",
|
step_id="authenticate",
|
||||||
data_schema=vol.Schema({vol.Required("password"): str}),
|
data_schema=vol.Schema({vol.Required("password"): str}),
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -623,9 +673,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return "connection_error"
|
return "connection_error"
|
||||||
finally:
|
finally:
|
||||||
await cli.disconnect(force=True)
|
await cli.disconnect(force=True)
|
||||||
self._name = self._device_info.friendly_name or self._device_info.name
|
|
||||||
self._device_name = self._device_info.name
|
|
||||||
self._device_mac = format_mac(self._device_info.mac_address)
|
self._device_mac = format_mac(self._device_info.mac_address)
|
||||||
|
self._device_name = self._device_info.name
|
||||||
|
self._name = self._device_info.friendly_name or self._device_info.name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def fetch_device_info(self) -> str | None:
|
async def fetch_device_info(self) -> str | None:
|
||||||
@ -640,7 +690,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
mac_address = format_mac(self._device_info.mac_address)
|
mac_address = format_mac(self._device_info.mac_address)
|
||||||
await self.async_set_unique_id(mac_address, raise_on_progress=False)
|
await self.async_set_unique_id(mac_address, raise_on_progress=False)
|
||||||
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
|
@ -224,7 +224,16 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||||
)
|
)
|
||||||
|
if entity_info.name:
|
||||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
||||||
|
else:
|
||||||
|
# https://github.com/home-assistant/core/issues/132532
|
||||||
|
# If name is not set, ESPHome will use the sanitized friendly name
|
||||||
|
# as the name, however we want to use the original object_id
|
||||||
|
# as the entity_id before it is sanitized since the sanitizer
|
||||||
|
# is not utf-8 aware. In this case, its always going to be
|
||||||
|
# an empty string so we drop the object_id.
|
||||||
|
self.entity_id = f"{domain}.{device_info.name}"
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
@ -260,7 +269,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||||||
self._static_info = static_info
|
self._static_info = static_info
|
||||||
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
||||||
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
||||||
self._attr_name = static_info.name
|
# https://github.com/home-assistant/core/issues/132532
|
||||||
|
# If the name is "", we need to set it to None since otherwise
|
||||||
|
# the friendly_name will be "{friendly_name} " with a trailing
|
||||||
|
# space. ESPHome uses protobuf under the hood, and an empty field
|
||||||
|
# gets a default value of "".
|
||||||
|
self._attr_name = static_info.name if static_info.name else None
|
||||||
if entity_category := static_info.entity_category:
|
if entity_category := static_info.entity_category:
|
||||||
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
|
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
|
||||||
else:
|
else:
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
|
||||||
|
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
||||||
@ -41,7 +43,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "Passwords are deprecated and will be removed in a future version. Please update your ESPHome device YAML configuration to use an encryption key instead."
|
"password": "Passwords are deprecated and will be removed in a future version. Please update your ESPHome device YAML configuration to use an encryption key instead."
|
||||||
},
|
},
|
||||||
"description": "Please enter the password you set in your ESPHome device YAML configuration for {name}."
|
"description": "Please enter the password you set in your ESPHome device YAML configuration for `{name}`."
|
||||||
},
|
},
|
||||||
"encryption_key": {
|
"encryption_key": {
|
||||||
"data": {
|
"data": {
|
||||||
@ -50,7 +52,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"noise_psk": "The encryption key is used to encrypt the connection between Home Assistant and the ESPHome device. You can find this in the api: section of your ESPHome device YAML configuration."
|
"noise_psk": "The encryption key is used to encrypt the connection between Home Assistant and the ESPHome device. You can find this in the api: section of your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
"description": "Please enter the encryption key for `{name}`. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"data": {
|
"data": {
|
||||||
@ -59,10 +61,10 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"noise_psk": "[%key:component::esphome::config::step::encryption_key::data_description::noise_psk%]"
|
"noise_psk": "[%key:component::esphome::config::step::encryption_key::data_description::noise_psk%]"
|
||||||
},
|
},
|
||||||
"description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
"description": "The ESPHome device `{name}` enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"reauth_encryption_removed_confirm": {
|
"reauth_encryption_removed_confirm": {
|
||||||
"description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
"description": "The ESPHome device `{name}` disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
||||||
},
|
},
|
||||||
"discovery_confirm": {
|
"discovery_confirm": {
|
||||||
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
||||||
|
@ -184,7 +184,7 @@
|
|||||||
"running": "Running",
|
"running": "Running",
|
||||||
"standby": "[%key:common::state::standby%]",
|
"standby": "[%key:common::state::standby%]",
|
||||||
"bootloading": "Bootloading",
|
"bootloading": "Bootloading",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"idle": "[%key:common::state::idle%]",
|
"idle": "[%key:common::state::idle%]",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"sleeping": "Sleeping"
|
"sleeping": "Sleeping"
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"name": "Inverter operation mode",
|
"name": "Inverter operation mode",
|
||||||
"state": {
|
"state": {
|
||||||
"general": "General mode",
|
"general": "General mode",
|
||||||
"off_grid": "Off grid mode",
|
"off_grid": "Off-grid mode",
|
||||||
"backup": "Backup mode",
|
"backup": "Backup mode",
|
||||||
"eco": "Eco mode",
|
"eco": "Eco mode",
|
||||||
"peak_shaving": "Peak shaving mode",
|
"peak_shaving": "Peak shaving mode",
|
||||||
|
@ -157,9 +157,6 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
|||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if not self.is_on or not kwargs:
|
|
||||||
await self.coordinator.turn_on(self._device)
|
|
||||||
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
brightness: int = int((float(kwargs[ATTR_BRIGHTNESS]) / 255.0) * 100.0)
|
brightness: int = int((float(kwargs[ATTR_BRIGHTNESS]) / 255.0) * 100.0)
|
||||||
await self.coordinator.set_brightness(self._device, brightness)
|
await self.coordinator.set_brightness(self._device, brightness)
|
||||||
@ -187,6 +184,9 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
|||||||
self._save_last_color_state()
|
self._save_last_color_state()
|
||||||
await self.coordinator.set_scene(self._device, effect)
|
await self.coordinator.set_scene(self._device, effect)
|
||||||
|
|
||||||
|
if not self.is_on or not kwargs:
|
||||||
|
await self.coordinator.turn_on(self._device)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
@ -54,7 +54,7 @@ class HistoryStats:
|
|||||||
self._period = (MIN_TIME_UTC, MIN_TIME_UTC)
|
self._period = (MIN_TIME_UTC, MIN_TIME_UTC)
|
||||||
self._state: HistoryStatsState = HistoryStatsState(None, None, self._period)
|
self._state: HistoryStatsState = HistoryStatsState(None, None, self._period)
|
||||||
self._history_current_period: list[HistoryState] = []
|
self._history_current_period: list[HistoryState] = []
|
||||||
self._previous_run_before_start = False
|
self._has_recorder_data = False
|
||||||
self._entity_states = set(entity_states)
|
self._entity_states = set(entity_states)
|
||||||
self._duration = duration
|
self._duration = duration
|
||||||
self._start = start
|
self._start = start
|
||||||
@ -88,20 +88,20 @@ class HistoryStats:
|
|||||||
if current_period_start_timestamp > now_timestamp:
|
if current_period_start_timestamp > now_timestamp:
|
||||||
# History cannot tell the future
|
# History cannot tell the future
|
||||||
self._history_current_period = []
|
self._history_current_period = []
|
||||||
self._previous_run_before_start = True
|
self._has_recorder_data = False
|
||||||
self._state = HistoryStatsState(None, None, self._period)
|
self._state = HistoryStatsState(None, None, self._period)
|
||||||
return self._state
|
return self._state
|
||||||
#
|
#
|
||||||
# We avoid querying the database if the below did NOT happen:
|
# We avoid querying the database if the below did NOT happen:
|
||||||
#
|
#
|
||||||
# - The previous run happened before the start time
|
# - No previous run occurred (uninitialized)
|
||||||
# - The start time changed
|
# - The start time moved back in time
|
||||||
# - The period shrank in size
|
# - The end time moved back in time
|
||||||
# - The previous period ended before now
|
# - The previous period ended before now
|
||||||
#
|
#
|
||||||
if (
|
if (
|
||||||
not self._previous_run_before_start
|
self._has_recorder_data
|
||||||
and current_period_start_timestamp == previous_period_start_timestamp
|
and current_period_start_timestamp >= previous_period_start_timestamp
|
||||||
and (
|
and (
|
||||||
current_period_end_timestamp == previous_period_end_timestamp
|
current_period_end_timestamp == previous_period_end_timestamp
|
||||||
or (
|
or (
|
||||||
@ -110,6 +110,12 @@ class HistoryStats:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
start_changed = (
|
||||||
|
current_period_start_timestamp != previous_period_start_timestamp
|
||||||
|
)
|
||||||
|
if start_changed:
|
||||||
|
self._prune_history_cache(current_period_start_timestamp)
|
||||||
|
|
||||||
new_data = False
|
new_data = False
|
||||||
if event and (new_state := event.data["new_state"]) is not None:
|
if event and (new_state := event.data["new_state"]) is not None:
|
||||||
if (
|
if (
|
||||||
@ -121,7 +127,11 @@ class HistoryStats:
|
|||||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||||
)
|
)
|
||||||
new_data = True
|
new_data = True
|
||||||
if not new_data and current_period_end_timestamp < now_timestamp:
|
if (
|
||||||
|
not new_data
|
||||||
|
and current_period_end_timestamp < now_timestamp
|
||||||
|
and not start_changed
|
||||||
|
):
|
||||||
# If period has not changed and current time after the period end...
|
# If period has not changed and current time after the period end...
|
||||||
# Don't compute anything as the value cannot have changed
|
# Don't compute anything as the value cannot have changed
|
||||||
return self._state
|
return self._state
|
||||||
@ -139,7 +149,7 @@ class HistoryStats:
|
|||||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._previous_run_before_start = False
|
self._has_recorder_data = True
|
||||||
|
|
||||||
seconds_matched, match_count = self._async_compute_seconds_and_changes(
|
seconds_matched, match_count = self._async_compute_seconds_and_changes(
|
||||||
now_timestamp,
|
now_timestamp,
|
||||||
@ -223,3 +233,18 @@ class HistoryStats:
|
|||||||
# Save value in seconds
|
# Save value in seconds
|
||||||
seconds_matched = elapsed
|
seconds_matched = elapsed
|
||||||
return seconds_matched, match_count
|
return seconds_matched, match_count
|
||||||
|
|
||||||
|
def _prune_history_cache(self, start_timestamp: float) -> None:
|
||||||
|
"""Remove unnecessary old data from the history state cache from previous runs.
|
||||||
|
|
||||||
|
Update the timestamp of the last record from before the start to the current start time.
|
||||||
|
"""
|
||||||
|
trim_count = 0
|
||||||
|
for i, history_state in enumerate(self._history_current_period):
|
||||||
|
if history_state.last_changed >= start_timestamp:
|
||||||
|
break
|
||||||
|
history_state.last_changed = start_timestamp
|
||||||
|
if i > 0:
|
||||||
|
trim_count += 1
|
||||||
|
if trim_count: # Don't slice if no data was removed
|
||||||
|
self._history_current_period = self._history_current_period[trim_count:]
|
||||||
|
@ -1536,7 +1536,7 @@
|
|||||||
"pause": "[%key:common::state::paused%]",
|
"pause": "[%key:common::state::paused%]",
|
||||||
"actionrequired": "Action required",
|
"actionrequired": "Action required",
|
||||||
"finished": "Finished",
|
"finished": "Finished",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"aborting": "Aborting"
|
"aborting": "Aborting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1587,7 +1587,7 @@
|
|||||||
"streaminglocal": "Streaming local",
|
"streaminglocal": "Streaming local",
|
||||||
"streamingcloud": "Streaming cloud",
|
"streamingcloud": "Streaming cloud",
|
||||||
"streaminglocal_and_cloud": "Streaming local and cloud",
|
"streaminglocal_and_cloud": "Streaming local and cloud",
|
||||||
"error": "Error"
|
"error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"last_selected_map": {
|
"last_selected_map": {
|
||||||
|
@ -61,7 +61,7 @@ reload_config_entry:
|
|||||||
required: false
|
required: false
|
||||||
example: 8955375327824e14ba89e4b29cc3ec9a
|
example: 8955375327824e14ba89e4b29cc3ec9a
|
||||||
selector:
|
selector:
|
||||||
text:
|
config_entry:
|
||||||
|
|
||||||
save_persistent_states:
|
save_persistent_states:
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"preferred_network_mode": {
|
"preferred_network_mode": {
|
||||||
"default": "mdi:transmission-tower"
|
"default": "mdi:antenna"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["huawei_lte_api.Session"],
|
"loggers": ["huawei_lte_api.Session"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"huawei-lte-api==1.10.0",
|
"huawei-lte-api==1.11.0",
|
||||||
"stringcase==1.2.0",
|
"stringcase==1.2.0",
|
||||||
"url-normalize==2.2.0"
|
"url-normalize==2.2.0"
|
||||||
],
|
],
|
||||||
|
@ -181,7 +181,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"cell_id": HuaweiSensorEntityDescription(
|
"cell_id": HuaweiSensorEntityDescription(
|
||||||
key="cell_id",
|
key="cell_id",
|
||||||
translation_key="cell_id",
|
translation_key="cell_id",
|
||||||
icon="mdi:transmission-tower",
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"cqi0": HuaweiSensorEntityDescription(
|
"cqi0": HuaweiSensorEntityDescription(
|
||||||
@ -230,6 +230,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"enodeb_id": HuaweiSensorEntityDescription(
|
"enodeb_id": HuaweiSensorEntityDescription(
|
||||||
key="enodeb_id",
|
key="enodeb_id",
|
||||||
translation_key="enodeb_id",
|
translation_key="enodeb_id",
|
||||||
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"lac": HuaweiSensorEntityDescription(
|
"lac": HuaweiSensorEntityDescription(
|
||||||
@ -364,7 +365,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"pci": HuaweiSensorEntityDescription(
|
"pci": HuaweiSensorEntityDescription(
|
||||||
key="pci",
|
key="pci",
|
||||||
translation_key="pci",
|
translation_key="pci",
|
||||||
icon="mdi:transmission-tower",
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"plmn": HuaweiSensorEntityDescription(
|
"plmn": HuaweiSensorEntityDescription(
|
||||||
|
@ -40,8 +40,7 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
||||||
|
|
||||||
ERROR_KEY_LIST = [
|
ERROR_KEYS = [
|
||||||
"no_error",
|
|
||||||
"alarm_mower_in_motion",
|
"alarm_mower_in_motion",
|
||||||
"alarm_mower_lifted",
|
"alarm_mower_lifted",
|
||||||
"alarm_mower_stopped",
|
"alarm_mower_stopped",
|
||||||
@ -50,13 +49,11 @@ ERROR_KEY_LIST = [
|
|||||||
"alarm_outside_geofence",
|
"alarm_outside_geofence",
|
||||||
"angular_sensor_problem",
|
"angular_sensor_problem",
|
||||||
"battery_problem",
|
"battery_problem",
|
||||||
"battery_problem",
|
|
||||||
"battery_restriction_due_to_ambient_temperature",
|
"battery_restriction_due_to_ambient_temperature",
|
||||||
"can_error",
|
"can_error",
|
||||||
"charging_current_too_high",
|
"charging_current_too_high",
|
||||||
"charging_station_blocked",
|
"charging_station_blocked",
|
||||||
"charging_system_problem",
|
"charging_system_problem",
|
||||||
"charging_system_problem",
|
|
||||||
"collision_sensor_defect",
|
"collision_sensor_defect",
|
||||||
"collision_sensor_error",
|
"collision_sensor_error",
|
||||||
"collision_sensor_problem_front",
|
"collision_sensor_problem_front",
|
||||||
@ -67,24 +64,18 @@ ERROR_KEY_LIST = [
|
|||||||
"connection_changed",
|
"connection_changed",
|
||||||
"connection_not_changed",
|
"connection_not_changed",
|
||||||
"connectivity_problem",
|
"connectivity_problem",
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_settings_restored",
|
"connectivity_settings_restored",
|
||||||
"cutting_drive_motor_1_defect",
|
"cutting_drive_motor_1_defect",
|
||||||
"cutting_drive_motor_2_defect",
|
"cutting_drive_motor_2_defect",
|
||||||
"cutting_drive_motor_3_defect",
|
"cutting_drive_motor_3_defect",
|
||||||
"cutting_height_blocked",
|
"cutting_height_blocked",
|
||||||
"cutting_height_problem",
|
|
||||||
"cutting_height_problem_curr",
|
"cutting_height_problem_curr",
|
||||||
"cutting_height_problem_dir",
|
"cutting_height_problem_dir",
|
||||||
"cutting_height_problem_drive",
|
"cutting_height_problem_drive",
|
||||||
|
"cutting_height_problem",
|
||||||
"cutting_motor_problem",
|
"cutting_motor_problem",
|
||||||
"cutting_stopped_slope_too_steep",
|
"cutting_stopped_slope_too_steep",
|
||||||
"cutting_system_blocked",
|
"cutting_system_blocked",
|
||||||
"cutting_system_blocked",
|
|
||||||
"cutting_system_imbalance_warning",
|
"cutting_system_imbalance_warning",
|
||||||
"cutting_system_major_imbalance",
|
"cutting_system_major_imbalance",
|
||||||
"destination_not_reachable",
|
"destination_not_reachable",
|
||||||
@ -92,13 +83,9 @@ ERROR_KEY_LIST = [
|
|||||||
"docking_sensor_defect",
|
"docking_sensor_defect",
|
||||||
"electronic_problem",
|
"electronic_problem",
|
||||||
"empty_battery",
|
"empty_battery",
|
||||||
MowerStates.ERROR.lower(),
|
|
||||||
MowerStates.ERROR_AT_POWER_UP.lower(),
|
|
||||||
MowerStates.FATAL_ERROR.lower(),
|
|
||||||
"folding_cutting_deck_sensor_defect",
|
"folding_cutting_deck_sensor_defect",
|
||||||
"folding_sensor_activated",
|
"folding_sensor_activated",
|
||||||
"geofence_problem",
|
"geofence_problem",
|
||||||
"geofence_problem",
|
|
||||||
"gps_navigation_problem",
|
"gps_navigation_problem",
|
||||||
"guide_1_not_found",
|
"guide_1_not_found",
|
||||||
"guide_2_not_found",
|
"guide_2_not_found",
|
||||||
@ -116,7 +103,6 @@ ERROR_KEY_LIST = [
|
|||||||
"lift_sensor_defect",
|
"lift_sensor_defect",
|
||||||
"lifted",
|
"lifted",
|
||||||
"limited_cutting_height_range",
|
"limited_cutting_height_range",
|
||||||
"limited_cutting_height_range",
|
|
||||||
"loop_sensor_defect",
|
"loop_sensor_defect",
|
||||||
"loop_sensor_problem_front",
|
"loop_sensor_problem_front",
|
||||||
"loop_sensor_problem_left",
|
"loop_sensor_problem_left",
|
||||||
@ -129,6 +115,7 @@ ERROR_KEY_LIST = [
|
|||||||
"no_accurate_position_from_satellites",
|
"no_accurate_position_from_satellites",
|
||||||
"no_confirmed_position",
|
"no_confirmed_position",
|
||||||
"no_drive",
|
"no_drive",
|
||||||
|
"no_error",
|
||||||
"no_loop_signal",
|
"no_loop_signal",
|
||||||
"no_power_in_charging_station",
|
"no_power_in_charging_station",
|
||||||
"no_response_from_charger",
|
"no_response_from_charger",
|
||||||
@ -139,9 +126,6 @@ ERROR_KEY_LIST = [
|
|||||||
"safety_function_faulty",
|
"safety_function_faulty",
|
||||||
"settings_restored",
|
"settings_restored",
|
||||||
"sim_card_locked",
|
"sim_card_locked",
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_not_found",
|
"sim_card_not_found",
|
||||||
"sim_card_requires_pin",
|
"sim_card_requires_pin",
|
||||||
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
||||||
@ -151,13 +135,6 @@ ERROR_KEY_LIST = [
|
|||||||
"stuck_in_charging_station",
|
"stuck_in_charging_station",
|
||||||
"switch_cord_problem",
|
"switch_cord_problem",
|
||||||
"temporary_battery_problem",
|
"temporary_battery_problem",
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"tilt_sensor_problem",
|
"tilt_sensor_problem",
|
||||||
"too_high_discharge_current",
|
"too_high_discharge_current",
|
||||||
"too_high_internal_current",
|
"too_high_internal_current",
|
||||||
@ -189,11 +166,19 @@ ERROR_KEY_LIST = [
|
|||||||
"zone_generator_problem",
|
"zone_generator_problem",
|
||||||
]
|
]
|
||||||
|
|
||||||
ERROR_STATES = {
|
ERROR_STATES = [
|
||||||
MowerStates.ERROR,
|
|
||||||
MowerStates.ERROR_AT_POWER_UP,
|
MowerStates.ERROR_AT_POWER_UP,
|
||||||
|
MowerStates.ERROR,
|
||||||
MowerStates.FATAL_ERROR,
|
MowerStates.FATAL_ERROR,
|
||||||
}
|
MowerStates.OFF,
|
||||||
|
MowerStates.STOPPED,
|
||||||
|
MowerStates.WAIT_POWER_UP,
|
||||||
|
MowerStates.WAIT_UPDATING,
|
||||||
|
]
|
||||||
|
|
||||||
|
ERROR_KEY_LIST = list(
|
||||||
|
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||||
|
)
|
||||||
|
|
||||||
RESTRICTED_REASONS: list = [
|
RESTRICTED_REASONS: list = [
|
||||||
RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
|
RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
|
||||||
@ -292,6 +277,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="cutting_blade_usage_time",
|
key="cutting_blade_usage_time",
|
||||||
translation_key="cutting_blade_usage_time",
|
translation_key="cutting_blade_usage_time",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
@ -302,6 +288,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="downtime",
|
key="downtime",
|
||||||
translation_key="downtime",
|
translation_key="downtime",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -386,6 +373,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="uptime",
|
key="uptime",
|
||||||
translation_key="uptime",
|
translation_key="uptime",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
|
@ -106,10 +106,10 @@
|
|||||||
"cutting_drive_motor_2_defect": "Cutting drive motor 2 defect",
|
"cutting_drive_motor_2_defect": "Cutting drive motor 2 defect",
|
||||||
"cutting_drive_motor_3_defect": "Cutting drive motor 3 defect",
|
"cutting_drive_motor_3_defect": "Cutting drive motor 3 defect",
|
||||||
"cutting_height_blocked": "Cutting height blocked",
|
"cutting_height_blocked": "Cutting height blocked",
|
||||||
"cutting_height_problem": "Cutting height problem",
|
|
||||||
"cutting_height_problem_curr": "Cutting height problem, curr",
|
"cutting_height_problem_curr": "Cutting height problem, curr",
|
||||||
"cutting_height_problem_dir": "Cutting height problem, dir",
|
"cutting_height_problem_dir": "Cutting height problem, dir",
|
||||||
"cutting_height_problem_drive": "Cutting height problem, drive",
|
"cutting_height_problem_drive": "Cutting height problem, drive",
|
||||||
|
"cutting_height_problem": "Cutting height problem",
|
||||||
"cutting_motor_problem": "Cutting motor problem",
|
"cutting_motor_problem": "Cutting motor problem",
|
||||||
"cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep",
|
"cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep",
|
||||||
"cutting_system_blocked": "Cutting system blocked",
|
"cutting_system_blocked": "Cutting system blocked",
|
||||||
@ -120,8 +120,8 @@
|
|||||||
"docking_sensor_defect": "Docking sensor defect",
|
"docking_sensor_defect": "Docking sensor defect",
|
||||||
"electronic_problem": "Electronic problem",
|
"electronic_problem": "Electronic problem",
|
||||||
"empty_battery": "Empty battery",
|
"empty_battery": "Empty battery",
|
||||||
"error": "Error",
|
|
||||||
"error_at_power_up": "Error at power up",
|
"error_at_power_up": "Error at power up",
|
||||||
|
"error": "[%key:common::state::error%]",
|
||||||
"fatal_error": "Fatal error",
|
"fatal_error": "Fatal error",
|
||||||
"folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect",
|
"folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect",
|
||||||
"folding_sensor_activated": "Folding sensor activated",
|
"folding_sensor_activated": "Folding sensor activated",
|
||||||
@ -159,6 +159,7 @@
|
|||||||
"no_loop_signal": "No loop signal",
|
"no_loop_signal": "No loop signal",
|
||||||
"no_power_in_charging_station": "No power in charging station",
|
"no_power_in_charging_station": "No power in charging station",
|
||||||
"no_response_from_charger": "No response from charger",
|
"no_response_from_charger": "No response from charger",
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
"outside_working_area": "Outside working area",
|
"outside_working_area": "Outside working area",
|
||||||
"poor_signal_quality": "Poor signal quality",
|
"poor_signal_quality": "Poor signal quality",
|
||||||
"reference_station_communication_problem": "Reference station communication problem",
|
"reference_station_communication_problem": "Reference station communication problem",
|
||||||
@ -172,6 +173,7 @@
|
|||||||
"slope_too_steep": "Slope too steep",
|
"slope_too_steep": "Slope too steep",
|
||||||
"sms_could_not_be_sent": "SMS could not be sent",
|
"sms_could_not_be_sent": "SMS could not be sent",
|
||||||
"stop_button_problem": "STOP button problem",
|
"stop_button_problem": "STOP button problem",
|
||||||
|
"stopped": "[%key:common::state::stopped%]",
|
||||||
"stuck_in_charging_station": "Stuck in charging station",
|
"stuck_in_charging_station": "Stuck in charging station",
|
||||||
"switch_cord_problem": "Switch cord problem",
|
"switch_cord_problem": "Switch cord problem",
|
||||||
"temporary_battery_problem": "Temporary battery problem",
|
"temporary_battery_problem": "Temporary battery problem",
|
||||||
@ -187,6 +189,8 @@
|
|||||||
"unexpected_cutting_height_adj": "Unexpected cutting height adjustment",
|
"unexpected_cutting_height_adj": "Unexpected cutting height adjustment",
|
||||||
"unexpected_error": "Unexpected error",
|
"unexpected_error": "Unexpected error",
|
||||||
"upside_down": "Upside down",
|
"upside_down": "Upside down",
|
||||||
|
"wait_power_up": "Wait power up",
|
||||||
|
"wait_updating": "Wait updating",
|
||||||
"weak_gps_signal": "Weak GPS signal",
|
"weak_gps_signal": "Weak GPS signal",
|
||||||
"wheel_drive_problem_left": "Left wheel drive problem",
|
"wheel_drive_problem_left": "Left wheel drive problem",
|
||||||
"wheel_drive_problem_rear_left": "Rear left wheel drive problem",
|
"wheel_drive_problem_rear_left": "Rear left wheel drive problem",
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"warming": "Warming",
|
"warming": "Warming",
|
||||||
"cooling": "Cooling",
|
"cooling": "Cooling",
|
||||||
"error": "Error"
|
"error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"_": {
|
"_": {
|
||||||
"name": "[%key:component::lawn_mower::title%]",
|
"name": "[%key:component::lawn_mower::title%]",
|
||||||
"state": {
|
"state": {
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
"mowing": "Mowing",
|
"mowing": "Mowing",
|
||||||
"docked": "Docked",
|
"docked": "Docked",
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"connected": "[%key:common::state::connected%]",
|
"connected": "[%key:common::state::connected%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"locked": "[%key:common::state::locked%]",
|
"locked": "[%key:common::state::locked%]",
|
||||||
"need_auth": "Waiting for authentication",
|
"need_auth": "Waiting for authentication",
|
||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"ocpp": "OCPP",
|
"ocpp": "OCPP",
|
||||||
"overtemperature": "Overtemperature",
|
"overtemperature": "Overtemperature",
|
||||||
"switching_phases": "Switching phases",
|
"switching_phases": "Switching phases",
|
||||||
"1p_charging_disabled": "1p charging disabled"
|
"1p_charging_disabled": "1P charging disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"breaker_current": {
|
"breaker_current": {
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["linkplay"],
|
"loggers": ["linkplay"],
|
||||||
"requirements": ["python-linkplay==0.2.3"],
|
"requirements": ["python-linkplay==0.2.4"],
|
||||||
"zeroconf": ["_linkplay._tcp.local."]
|
"zeroconf": ["_linkplay._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,10 @@ from .const import (
|
|||||||
SERVICE_SET_LEVEL,
|
SERVICE_SET_LEVEL,
|
||||||
)
|
)
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
DATA_LOGGER,
|
||||||
LoggerDomainConfig,
|
LoggerDomainConfig,
|
||||||
LoggerSettings,
|
LoggerSettings,
|
||||||
|
_clear_logger_overwrites, # noqa: F401
|
||||||
set_default_log_level,
|
set_default_log_level,
|
||||||
set_log_levels,
|
set_log_levels,
|
||||||
)
|
)
|
||||||
@ -54,7 +56,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
settings = LoggerSettings(hass, config)
|
settings = LoggerSettings(hass, config)
|
||||||
|
|
||||||
domain_config = hass.data[DOMAIN] = LoggerDomainConfig({}, settings)
|
domain_config = hass.data[DATA_LOGGER] = LoggerDomainConfig({}, settings)
|
||||||
logging.setLoggerClass(_get_logger_class(domain_config.overrides))
|
logging.setLoggerClass(_get_logger_class(domain_config.overrides))
|
||||||
|
|
||||||
websocket_api.async_load_websocket_api(hass)
|
websocket_api.async_load_websocket_api(hass)
|
||||||
|
@ -9,13 +9,14 @@ from dataclasses import asdict, dataclass
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.const import EVENT_LOGGING_CHANGED
|
from homeassistant.const import EVENT_LOGGING_CHANGED
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -28,6 +29,8 @@ from .const import (
|
|||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DATA_LOGGER: HassKey[LoggerDomainConfig] = HassKey(DOMAIN)
|
||||||
|
|
||||||
SAVE_DELAY = 15.0
|
SAVE_DELAY = 15.0
|
||||||
# At startup, we want to save after a long delay to avoid
|
# At startup, we want to save after a long delay to avoid
|
||||||
# saving while the system is still starting up. If the system
|
# saving while the system is still starting up. If the system
|
||||||
@ -39,12 +42,6 @@ SAVE_DELAY = 15.0
|
|||||||
SAVE_DELAY_LONG = 180.0
|
SAVE_DELAY_LONG = 180.0
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig:
|
|
||||||
"""Return the domain config."""
|
|
||||||
return cast(LoggerDomainConfig, hass.data[DOMAIN])
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def set_default_log_level(hass: HomeAssistant, level: int) -> None:
|
def set_default_log_level(hass: HomeAssistant, level: int) -> None:
|
||||||
"""Set the default log level for components."""
|
"""Set the default log level for components."""
|
||||||
@ -55,7 +52,7 @@ def set_default_log_level(hass: HomeAssistant, level: int) -> None:
|
|||||||
@callback
|
@callback
|
||||||
def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None:
|
def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None:
|
||||||
"""Set the specified log levels."""
|
"""Set the specified log levels."""
|
||||||
async_get_domain_config(hass).overrides.update(logpoints)
|
hass.data[DATA_LOGGER].overrides.update(logpoints)
|
||||||
for key, value in logpoints.items():
|
for key, value in logpoints.items():
|
||||||
_set_log_level(logging.getLogger(key), value)
|
_set_log_level(logging.getLogger(key), value)
|
||||||
hass.bus.async_fire(EVENT_LOGGING_CHANGED)
|
hass.bus.async_fire(EVENT_LOGGING_CHANGED)
|
||||||
@ -78,6 +75,12 @@ def _chattiest_log_level(level1: int, level2: int) -> int:
|
|||||||
return min(level1, level2)
|
return min(level1, level2)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _clear_logger_overwrites(hass: HomeAssistant) -> None:
|
||||||
|
"""Clear logger overwrites. Used for testing."""
|
||||||
|
hass.data[DATA_LOGGER].overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
async def get_integration_loggers(hass: HomeAssistant, domain: str) -> set[str]:
|
async def get_integration_loggers(hass: HomeAssistant, domain: str) -> set[str]:
|
||||||
"""Get loggers for an integration."""
|
"""Get loggers for an integration."""
|
||||||
loggers: set[str] = {f"homeassistant.components.{domain}"}
|
loggers: set[str] = {f"homeassistant.components.{domain}"}
|
||||||
|
@ -12,10 +12,10 @@ from homeassistant.setup import async_get_loaded_integrations
|
|||||||
|
|
||||||
from .const import LOGSEVERITY
|
from .const import LOGSEVERITY
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
DATA_LOGGER,
|
||||||
LoggerSetting,
|
LoggerSetting,
|
||||||
LogPersistance,
|
LogPersistance,
|
||||||
LogSettingsType,
|
LogSettingsType,
|
||||||
async_get_domain_config,
|
|
||||||
get_logger,
|
get_logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ async def handle_integration_log_level(
|
|||||||
msg["id"], websocket_api.ERR_NOT_FOUND, "Integration not found"
|
msg["id"], websocket_api.ERR_NOT_FOUND, "Integration not found"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
await async_get_domain_config(hass).settings.async_update(
|
await hass.data[DATA_LOGGER].settings.async_update(
|
||||||
hass,
|
hass,
|
||||||
msg["integration"],
|
msg["integration"],
|
||||||
LoggerSetting(
|
LoggerSetting(
|
||||||
@ -93,7 +93,7 @@ async def handle_module_log_level(
|
|||||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle setting integration log level."""
|
"""Handle setting integration log level."""
|
||||||
await async_get_domain_config(hass).settings.async_update(
|
await hass.data[DATA_LOGGER].settings.async_update(
|
||||||
hass,
|
hass,
|
||||||
msg["module"],
|
msg["module"],
|
||||||
LoggerSetting(
|
LoggerSetting(
|
||||||
|
@ -270,7 +270,7 @@
|
|||||||
"stopped": "[%key:common::state::stopped%]",
|
"stopped": "[%key:common::state::stopped%]",
|
||||||
"running": "Running",
|
"running": "Running",
|
||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"seeking_charger": "Seeking charger",
|
"seeking_charger": "Seeking charger",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"docked": "Docked"
|
"docked": "Docked"
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pymiele"],
|
"loggers": ["pymiele"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pymiele==0.3.4"],
|
"requirements": ["pymiele==0.3.6"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -185,6 +185,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool
|
|||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_config_entry_device(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
device_entry: dr.DeviceEntry,
|
||||||
|
) -> bool:
|
||||||
|
"""Remove NUT config entry from a device."""
|
||||||
|
return not any(
|
||||||
|
identifier
|
||||||
|
for identifier in device_entry.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
and identifier[1] in config_entry.runtime_data.unique_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _async_update_listener(hass: HomeAssistant, entry: NutConfigEntry) -> None:
|
async def _async_update_listener(hass: HomeAssistant, entry: NutConfigEntry) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
@ -19,7 +19,6 @@ from homeassistant.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.data_entry_flow import AbortFlow
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ def _ups_schema(ups_list: dict[str, str]) -> vol.Schema:
|
|||||||
return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)})
|
return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)})
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
async def validate_input(data: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Validate the user input allows us to connect.
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
Data has the keys from _base_schema with values provided by the user.
|
Data has the keys from _base_schema with values provided by the user.
|
||||||
@ -303,7 +302,7 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
info: dict[str, Any] = {}
|
info: dict[str, Any] = {}
|
||||||
description_placeholders: dict[str, str] = {}
|
description_placeholders: dict[str, str] = {}
|
||||||
try:
|
try:
|
||||||
info = await validate_input(self.hass, config)
|
info = await validate_input(config)
|
||||||
except NUTLoginError:
|
except NUTLoginError:
|
||||||
errors[CONF_PASSWORD] = "invalid_auth"
|
errors[CONF_PASSWORD] = "invalid_auth"
|
||||||
except NUTError as ex:
|
except NUTError as ex:
|
||||||
@ -320,8 +319,6 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle reauth."""
|
"""Handle reauth."""
|
||||||
entry_id = self.context["entry_id"]
|
|
||||||
self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id)
|
|
||||||
return await self.async_step_reauth_confirm()
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
@ -330,17 +327,16 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle reauth input."""
|
"""Handle reauth input."""
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
existing_entry = self.reauth_entry
|
reauth_entry = self._get_reauth_entry()
|
||||||
assert existing_entry
|
reauth_data = reauth_entry.data
|
||||||
existing_data = existing_entry.data
|
|
||||||
description_placeholders: dict[str, str] = {
|
description_placeholders: dict[str, str] = {
|
||||||
CONF_HOST: existing_data[CONF_HOST],
|
CONF_HOST: reauth_data[CONF_HOST],
|
||||||
CONF_PORT: existing_data[CONF_PORT],
|
CONF_PORT: reauth_data[CONF_PORT],
|
||||||
}
|
}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
new_config = {
|
new_config = {
|
||||||
**existing_data,
|
**reauth_data,
|
||||||
# Username/password are optional and some servers
|
# Username/password are optional and some servers
|
||||||
# use ip based authentication and will fail if
|
# use ip based authentication and will fail if
|
||||||
# username/password are provided
|
# username/password are provided
|
||||||
@ -349,9 +345,7 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
_, errors, placeholders = await self._async_validate_or_error(new_config)
|
_, errors, placeholders = await self._async_validate_or_error(new_config)
|
||||||
if not errors:
|
if not errors:
|
||||||
return self.async_update_reload_and_abort(
|
return self.async_update_reload_and_abort(reauth_entry, data=new_config)
|
||||||
existing_entry, data=new_config
|
|
||||||
)
|
|
||||||
description_placeholders.update(placeholders)
|
description_placeholders.update(placeholders)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"button": {
|
|
||||||
"outlet_number_load_cycle": {
|
|
||||||
"default": "mdi:restart"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"ambient_humidity_status": {
|
"ambient_humidity_status": {
|
||||||
"default": "mdi:information-outline"
|
"default": "mdi:information-outline"
|
||||||
|
@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_UNKNOWN,
|
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfApparentPower,
|
UnitOfApparentPower,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
@ -1120,9 +1119,9 @@ class NUTSensor(NUTBaseEntity, SensorEntity):
|
|||||||
return status.get(self.entity_description.key)
|
return status.get(self.entity_description.key)
|
||||||
|
|
||||||
|
|
||||||
def _format_display_state(status: dict[str, str]) -> str:
|
def _format_display_state(status: dict[str, str]) -> str | None:
|
||||||
"""Return UPS display state."""
|
"""Return UPS display state."""
|
||||||
try:
|
try:
|
||||||
return ", ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split())
|
return ", ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return STATE_UNKNOWN
|
return None
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
"title": "Choose the NUT server UPS to monitor",
|
"title": "Choose the NUT server UPS to monitor",
|
||||||
"data": {
|
"data": {
|
||||||
"alias": "NUT server UPS name"
|
"alias": "NUT server UPS name"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"alias": "The UPS name configured on the NUT server."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
@ -27,6 +30,10 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"username": "[%key:component::nut::config::step::user::data_description::username%]",
|
||||||
|
"password": "[%key:component::nut::config::step::user::data_description::password%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reconfigure": {
|
"reconfigure": {
|
||||||
@ -48,6 +55,9 @@
|
|||||||
"title": "[%key:component::nut::config::step::ups::title%]",
|
"title": "[%key:component::nut::config::step::ups::title%]",
|
||||||
"data": {
|
"data": {
|
||||||
"alias": "[%key:component::nut::config::step::ups::data::alias%]"
|
"alias": "[%key:component::nut::config::step::ups::data::alias%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"alias": "[%key:component::nut::config::step::ups::data_description::alias%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,7 @@ from pyoverkiz.enums import APIType, OverkizState, UIClass, UIWidget
|
|||||||
from pyoverkiz.exceptions import (
|
from pyoverkiz.exceptions import (
|
||||||
BadCredentialsException,
|
BadCredentialsException,
|
||||||
MaintenanceException,
|
MaintenanceException,
|
||||||
|
NotAuthenticatedException,
|
||||||
NotSuchTokenException,
|
NotSuchTokenException,
|
||||||
TooManyRequestsException,
|
TooManyRequestsException,
|
||||||
)
|
)
|
||||||
@ -92,7 +93,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: OverkizDataConfigEntry)
|
|||||||
scenarios = await client.get_scenarios()
|
scenarios = await client.get_scenarios()
|
||||||
else:
|
else:
|
||||||
scenarios = []
|
scenarios = []
|
||||||
except (BadCredentialsException, NotSuchTokenException) as exception:
|
except (
|
||||||
|
BadCredentialsException,
|
||||||
|
NotSuchTokenException,
|
||||||
|
NotAuthenticatedException,
|
||||||
|
) as exception:
|
||||||
raise ConfigEntryAuthFailed("Invalid authentication") from exception
|
raise ConfigEntryAuthFailed("Invalid authentication") from exception
|
||||||
except TooManyRequestsException as exception:
|
except TooManyRequestsException as exception:
|
||||||
raise ConfigEntryNotReady("Too many requests, try again later") from exception
|
raise ConfigEntryNotReady("Too many requests, try again later") from exception
|
||||||
|
@ -13,6 +13,7 @@ from homeassistant.components.climate import (
|
|||||||
PRESET_NONE,
|
PRESET_NONE,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
|
HVACAction,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||||
@ -56,6 +57,12 @@ OVERKIZ_TO_HVAC_MODE: dict[str, HVACMode] = {
|
|||||||
OverkizCommandParam.INTERNAL: HVACMode.AUTO,
|
OverkizCommandParam.INTERNAL: HVACMode.AUTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
|
||||||
|
OverkizCommandParam.STANDBY: HVACAction.IDLE,
|
||||||
|
OverkizCommandParam.INCREASE: HVACAction.HEATING,
|
||||||
|
OverkizCommandParam.NONE: HVACAction.OFF,
|
||||||
|
}
|
||||||
|
|
||||||
HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()}
|
HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()}
|
||||||
|
|
||||||
TEMPERATURE_SENSOR_DEVICE_INDEX = 2
|
TEMPERATURE_SENSOR_DEVICE_INDEX = 2
|
||||||
@ -102,6 +109,14 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
|
|||||||
OverkizCommand.SET_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode]
|
OverkizCommand.SET_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self) -> HVACAction:
|
||||||
|
"""Return the current running hvac operation ie. heating, idle, off."""
|
||||||
|
states = self.device.states
|
||||||
|
if (state := states[OverkizState.CORE_REGULATION_MODE]) and state.value_as_str:
|
||||||
|
return OVERKIZ_TO_HVAC_ACTION[state.value_as_str]
|
||||||
|
return HVACAction.OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_mode(self) -> str | None:
|
def preset_mode(self) -> str | None:
|
||||||
"""Return the current preset mode, e.g., home, away, temp."""
|
"""Return the current preset mode, e.g., home, away, temp."""
|
||||||
|
@ -13,12 +13,12 @@ from pyoverkiz.exceptions import (
|
|||||||
BadCredentialsException,
|
BadCredentialsException,
|
||||||
CozyTouchBadCredentialsException,
|
CozyTouchBadCredentialsException,
|
||||||
MaintenanceException,
|
MaintenanceException,
|
||||||
|
NotAuthenticatedException,
|
||||||
NotSuchTokenException,
|
NotSuchTokenException,
|
||||||
TooManyAttemptsBannedException,
|
TooManyAttemptsBannedException,
|
||||||
TooManyRequestsException,
|
TooManyRequestsException,
|
||||||
UnknownUserException,
|
UnknownUserException,
|
||||||
)
|
)
|
||||||
from pyoverkiz.models import OverkizServer
|
|
||||||
from pyoverkiz.obfuscate import obfuscate_id
|
from pyoverkiz.obfuscate import obfuscate_id
|
||||||
from pyoverkiz.utils import generate_local_server, is_overkiz_gateway
|
from pyoverkiz.utils import generate_local_server, is_overkiz_gateway
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -31,7 +31,6 @@ from homeassistant.const import (
|
|||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
@ -39,15 +38,12 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
|||||||
from .const import CONF_API_TYPE, CONF_HUB, DEFAULT_SERVER, DOMAIN, LOGGER
|
from .const import CONF_API_TYPE, CONF_HUB, DEFAULT_SERVER, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
class DeveloperModeDisabled(HomeAssistantError):
|
|
||||||
"""Error to indicate Somfy Developer Mode is disabled."""
|
|
||||||
|
|
||||||
|
|
||||||
class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Overkiz (by Somfy)."""
|
"""Handle a config flow for Overkiz (by Somfy)."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
|
_verify_ssl: bool = True
|
||||||
_api_type: APIType = APIType.CLOUD
|
_api_type: APIType = APIType.CLOUD
|
||||||
_user: str | None = None
|
_user: str | None = None
|
||||||
_server: str = DEFAULT_SERVER
|
_server: str = DEFAULT_SERVER
|
||||||
@ -57,27 +53,36 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Validate user credentials."""
|
"""Validate user credentials."""
|
||||||
user_input[CONF_API_TYPE] = self._api_type
|
user_input[CONF_API_TYPE] = self._api_type
|
||||||
|
|
||||||
client = self._create_cloud_client(
|
if self._api_type == APIType.LOCAL:
|
||||||
|
user_input[CONF_VERIFY_SSL] = self._verify_ssl
|
||||||
|
session = async_create_clientsession(
|
||||||
|
self.hass, verify_ssl=user_input[CONF_VERIFY_SSL]
|
||||||
|
)
|
||||||
|
client = OverkizClient(
|
||||||
|
username="",
|
||||||
|
password="",
|
||||||
|
token=user_input[CONF_TOKEN],
|
||||||
|
session=session,
|
||||||
|
server=generate_local_server(host=user_input[CONF_HOST]),
|
||||||
|
verify_ssl=user_input[CONF_VERIFY_SSL],
|
||||||
|
)
|
||||||
|
else: # APIType.CLOUD
|
||||||
|
session = async_create_clientsession(self.hass)
|
||||||
|
client = OverkizClient(
|
||||||
username=user_input[CONF_USERNAME],
|
username=user_input[CONF_USERNAME],
|
||||||
password=user_input[CONF_PASSWORD],
|
password=user_input[CONF_PASSWORD],
|
||||||
server=SUPPORTED_SERVERS[user_input[CONF_HUB]],
|
server=SUPPORTED_SERVERS[user_input[CONF_HUB]],
|
||||||
|
session=session,
|
||||||
)
|
)
|
||||||
await client.login(register_event_listener=False)
|
|
||||||
|
|
||||||
# For Local API, we create and activate a local token
|
await client.login(register_event_listener=False)
|
||||||
if self._api_type == APIType.LOCAL:
|
|
||||||
user_input[CONF_TOKEN] = await self._create_local_api_token(
|
|
||||||
cloud_client=client,
|
|
||||||
host=user_input[CONF_HOST],
|
|
||||||
verify_ssl=user_input[CONF_VERIFY_SSL],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set main gateway id as unique id
|
# Set main gateway id as unique id
|
||||||
if gateways := await client.get_gateways():
|
if gateways := await client.get_gateways():
|
||||||
for gateway in gateways:
|
for gateway in gateways:
|
||||||
if is_overkiz_gateway(gateway.id):
|
if is_overkiz_gateway(gateway.id):
|
||||||
gateway_id = gateway.id
|
await self.async_set_unique_id(gateway.id, raise_on_progress=False)
|
||||||
await self.async_set_unique_id(gateway_id, raise_on_progress=False)
|
break
|
||||||
|
|
||||||
return user_input
|
return user_input
|
||||||
|
|
||||||
@ -141,15 +146,13 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
self._user = user_input[CONF_USERNAME]
|
self._user = user_input[CONF_USERNAME]
|
||||||
|
|
||||||
# inherit the server from previous step
|
|
||||||
user_input[CONF_HUB] = self._server
|
user_input[CONF_HUB] = self._server
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.async_validate_input(user_input)
|
await self.async_validate_input(user_input)
|
||||||
except TooManyRequestsException:
|
except TooManyRequestsException:
|
||||||
errors["base"] = "too_many_requests"
|
errors["base"] = "too_many_requests"
|
||||||
except BadCredentialsException as exception:
|
except (BadCredentialsException, NotAuthenticatedException) as exception:
|
||||||
# If authentication with CozyTouch auth server is valid, but token is invalid
|
# If authentication with CozyTouch auth server is valid, but token is invalid
|
||||||
# for Overkiz API server, the hardware is not supported.
|
# for Overkiz API server, the hardware is not supported.
|
||||||
if user_input[CONF_HUB] in {
|
if user_input[CONF_HUB] in {
|
||||||
@ -211,16 +214,18 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
self._host = user_input[CONF_HOST]
|
self._host = user_input[CONF_HOST]
|
||||||
self._user = user_input[CONF_USERNAME]
|
self._verify_ssl = user_input[CONF_VERIFY_SSL]
|
||||||
|
|
||||||
# inherit the server from previous step
|
|
||||||
user_input[CONF_HUB] = self._server
|
user_input[CONF_HUB] = self._server
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_input = await self.async_validate_input(user_input)
|
user_input = await self.async_validate_input(user_input)
|
||||||
except TooManyRequestsException:
|
except TooManyRequestsException:
|
||||||
errors["base"] = "too_many_requests"
|
errors["base"] = "too_many_requests"
|
||||||
except BadCredentialsException:
|
except (
|
||||||
|
BadCredentialsException,
|
||||||
|
NotSuchTokenException,
|
||||||
|
NotAuthenticatedException,
|
||||||
|
):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except ClientConnectorCertificateError as exception:
|
except ClientConnectorCertificateError as exception:
|
||||||
errors["base"] = "certificate_verify_failed"
|
errors["base"] = "certificate_verify_failed"
|
||||||
@ -232,10 +237,6 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "server_in_maintenance"
|
errors["base"] = "server_in_maintenance"
|
||||||
except TooManyAttemptsBannedException:
|
except TooManyAttemptsBannedException:
|
||||||
errors["base"] = "too_many_attempts"
|
errors["base"] = "too_many_attempts"
|
||||||
except NotSuchTokenException:
|
|
||||||
errors["base"] = "no_such_token"
|
|
||||||
except DeveloperModeDisabled:
|
|
||||||
errors["base"] = "developer_mode_disabled"
|
|
||||||
except UnknownUserException:
|
except UnknownUserException:
|
||||||
# Somfy Protect accounts are not supported since they don't use
|
# Somfy Protect accounts are not supported since they don't use
|
||||||
# the Overkiz API server. Login will return unknown user.
|
# the Overkiz API server. Login will return unknown user.
|
||||||
@ -264,9 +265,8 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST, default=self._host): str,
|
vol.Required(CONF_HOST, default=self._host): str,
|
||||||
vol.Required(CONF_USERNAME, default=self._user): str,
|
vol.Required(CONF_TOKEN): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_VERIFY_SSL, default=self._verify_ssl): bool,
|
||||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
@ -320,64 +320,15 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle reauth."""
|
"""Handle reauth."""
|
||||||
# overkiz entries always have unique IDs
|
# Overkiz entries always have unique IDs
|
||||||
self.context["title_placeholders"] = {"gateway_id": cast(str, self.unique_id)}
|
self.context["title_placeholders"] = {"gateway_id": cast(str, self.unique_id)}
|
||||||
|
|
||||||
self._user = entry_data[CONF_USERNAME]
|
|
||||||
self._server = entry_data[CONF_HUB]
|
|
||||||
self._api_type = entry_data.get(CONF_API_TYPE, APIType.CLOUD)
|
self._api_type = entry_data.get(CONF_API_TYPE, APIType.CLOUD)
|
||||||
|
self._server = entry_data[CONF_HUB]
|
||||||
|
|
||||||
if self._api_type == APIType.LOCAL:
|
if self._api_type == APIType.LOCAL:
|
||||||
self._host = entry_data[CONF_HOST]
|
self._host = entry_data[CONF_HOST]
|
||||||
|
self._verify_ssl = entry_data[CONF_VERIFY_SSL]
|
||||||
|
else:
|
||||||
|
self._user = entry_data[CONF_USERNAME]
|
||||||
|
|
||||||
return await self.async_step_user(dict(entry_data))
|
return await self.async_step_user(dict(entry_data))
|
||||||
|
|
||||||
def _create_cloud_client(
|
|
||||||
self, username: str, password: str, server: OverkizServer
|
|
||||||
) -> OverkizClient:
|
|
||||||
session = async_create_clientsession(self.hass)
|
|
||||||
return OverkizClient(
|
|
||||||
username=username, password=password, server=server, session=session
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _create_local_api_token(
|
|
||||||
self, cloud_client: OverkizClient, host: str, verify_ssl: bool
|
|
||||||
) -> str:
|
|
||||||
"""Create local API token."""
|
|
||||||
# Create session on Somfy cloud server to generate an access token for local API
|
|
||||||
gateways = await cloud_client.get_gateways()
|
|
||||||
|
|
||||||
gateway_id = ""
|
|
||||||
for gateway in gateways:
|
|
||||||
# Overkiz can return multiple gateways, but we only can generate a token
|
|
||||||
# for the main gateway.
|
|
||||||
if is_overkiz_gateway(gateway.id):
|
|
||||||
gateway_id = gateway.id
|
|
||||||
|
|
||||||
developer_mode = await cloud_client.get_setup_option(
|
|
||||||
f"developerMode-{gateway_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if developer_mode is None:
|
|
||||||
raise DeveloperModeDisabled
|
|
||||||
|
|
||||||
token = await cloud_client.generate_local_token(gateway_id)
|
|
||||||
await cloud_client.activate_local_token(
|
|
||||||
gateway_id=gateway_id, token=token, label="Home Assistant/local"
|
|
||||||
)
|
|
||||||
|
|
||||||
session = async_create_clientsession(self.hass, verify_ssl=verify_ssl)
|
|
||||||
|
|
||||||
# Local API
|
|
||||||
local_client = OverkizClient(
|
|
||||||
username="",
|
|
||||||
password="",
|
|
||||||
token=token,
|
|
||||||
session=session,
|
|
||||||
server=generate_local_server(host=host),
|
|
||||||
verify_ssl=verify_ssl,
|
|
||||||
)
|
|
||||||
|
|
||||||
await local_client.login()
|
|
||||||
|
|
||||||
return token
|
|
||||||
|
@ -79,7 +79,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
|
|||||||
"""Fetch Overkiz data via event listener."""
|
"""Fetch Overkiz data via event listener."""
|
||||||
try:
|
try:
|
||||||
events = await self.client.fetch_events()
|
events = await self.client.fetch_events()
|
||||||
except BadCredentialsException as exception:
|
except (BadCredentialsException, NotAuthenticatedException) as exception:
|
||||||
raise ConfigEntryAuthFailed("Invalid authentication.") from exception
|
raise ConfigEntryAuthFailed("Invalid authentication.") from exception
|
||||||
except TooManyConcurrentRequestsException as exception:
|
except TooManyConcurrentRequestsException as exception:
|
||||||
raise UpdateFailed("Too many concurrent requests.") from exception
|
raise UpdateFailed("Too many concurrent requests.") from exception
|
||||||
@ -98,7 +98,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
|
|||||||
try:
|
try:
|
||||||
await self.client.login()
|
await self.client.login()
|
||||||
self.devices = await self._get_devices()
|
self.devices = await self._get_devices()
|
||||||
except BadCredentialsException as exception:
|
except (BadCredentialsException, NotAuthenticatedException) as exception:
|
||||||
raise ConfigEntryAuthFailed("Invalid authentication.") from exception
|
raise ConfigEntryAuthFailed("Invalid authentication.") from exception
|
||||||
except TooManyRequestsException as exception:
|
except TooManyRequestsException as exception:
|
||||||
raise UpdateFailed("Too many requests, try again later.") from exception
|
raise UpdateFailed("Too many requests, try again later.") from exception
|
||||||
|
20
homeassistant/components/overkiz/icons.json
Normal file
20
homeassistant/components/overkiz/icons.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"climate": {
|
||||||
|
"overkiz": {
|
||||||
|
"state_attributes": {
|
||||||
|
"preset_mode": {
|
||||||
|
"state": {
|
||||||
|
"auto": "mdi:thermostat-auto",
|
||||||
|
"comfort-1": "mdi:thermometer",
|
||||||
|
"comfort-2": "mdi:thermometer-low",
|
||||||
|
"frost_protection": "mdi:snowflake",
|
||||||
|
"prog": "mdi:clock-outline",
|
||||||
|
"external": "mdi:remote"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||||
"requirements": ["pyoverkiz==1.17.0"],
|
"requirements": ["pyoverkiz==1.17.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_kizbox._tcp.local.",
|
"type": "_kizbox._tcp.local.",
|
||||||
|
@ -32,17 +32,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"local": {
|
"local": {
|
||||||
"description": "By activating the [Developer Mode of your TaHoma box](https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\nAfter activation, enter your application credentials and change the host to include your Gateway PIN or enter the IP address of your gateway.",
|
"description": "By activating the [Developer Mode of your TaHoma box](https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"token": "[%key:common::config_flow::data::api_token%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your Overkiz hub.",
|
"host": "The hostname or IP address of your Overkiz hub.",
|
||||||
"username": "The username of your cloud account (app).",
|
"token": "Token generated by the app used to control your device.",
|
||||||
"password": "The password of your cloud account (app).",
|
|
||||||
"verify_ssl": "Verify the SSL certificate. Select this only if you are connecting via the hostname."
|
"verify_ssl": "Verify the SSL certificate. Select this only if you are connecting via the hostname."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,8 +71,8 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"auto": "[%key:common::state::auto%]",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"manual": "[%key:common::state::manual%]",
|
"manual": "[%key:common::state::manual%]",
|
||||||
"comfort-1": "Comfort 1",
|
"comfort-1": "Comfort -1°C",
|
||||||
"comfort-2": "Comfort 2",
|
"comfort-2": "Comfort -2°C",
|
||||||
"drying": "Drying",
|
"drying": "Drying",
|
||||||
"external": "External",
|
"external": "External",
|
||||||
"freeze": "Freeze",
|
"freeze": "Freeze",
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
"name": "State",
|
"name": "State",
|
||||||
"state": {
|
"state": {
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"fault": "Fault",
|
"fault": "Fault",
|
||||||
"invalid": "Invalid",
|
"invalid": "Invalid",
|
||||||
"no_ev_connected": "No EV connected",
|
"no_ev_connected": "No EV connected",
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
"finished": "Finished",
|
"finished": "Finished",
|
||||||
"stopped": "[%key:common::state::stopped%]",
|
"stopped": "[%key:common::state::stopped%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"attention": "Attention",
|
"attention": "Attention",
|
||||||
"ready": "Ready"
|
"ready": "Ready"
|
||||||
}
|
}
|
||||||
|
@ -84,8 +84,10 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||||
|
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||||
|
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||||
|
@ -232,7 +232,7 @@
|
|||||||
"charging_problem": "Charging problem",
|
"charging_problem": "Charging problem",
|
||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
"spot_cleaning": "Spot cleaning",
|
"spot_cleaning": "Spot cleaning",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"shutting_down": "Shutting down",
|
"shutting_down": "Shutting down",
|
||||||
"updating": "Updating",
|
"updating": "Updating",
|
||||||
"docking": "Docking",
|
"docking": "Docking",
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyschlage==2024.11.0"]
|
"requirements": ["pyschlage==2025.4.0"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioshelly"],
|
"loggers": ["aioshelly"],
|
||||||
"requirements": ["aioshelly==13.4.1"],
|
"requirements": ["aioshelly==13.5.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_http._tcp.local.",
|
"type": "_http._tcp.local.",
|
||||||
|
@ -1,34 +1,10 @@
|
|||||||
"""Support to control a Salda Smarty XP/XV ventilation unit."""
|
"""Support to control a Salda Smarty XP/XV ventilation unit."""
|
||||||
|
|
||||||
import ipaddress
|
from homeassistant.const import Platform
|
||||||
import logging
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, Platform
|
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
|
||||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import SmartyConfigEntry, SmartyCoordinator
|
from .coordinator import SmartyConfigEntry, SmartyCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
|
|
||||||
vol.Optional(CONF_NAME, default="Smarty"): cv.string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
@ -38,54 +14,6 @@ PLATFORMS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
|
||||||
"""Create a smarty system."""
|
|
||||||
if config := hass_config.get(DOMAIN):
|
|
||||||
hass.async_create_task(_async_import(hass, config))
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
|
|
||||||
"""Set up the smarty environment."""
|
|
||||||
|
|
||||||
if not hass.config_entries.async_entries(DOMAIN):
|
|
||||||
# Start import flow
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
|
||||||
)
|
|
||||||
if result["type"] == FlowResultType.ABORT:
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
f"deprecated_yaml_import_issue_{result['reason']}",
|
|
||||||
breaks_in_ha_version="2025.5.0",
|
|
||||||
is_fixable=False,
|
|
||||||
issue_domain=DOMAIN,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
|
|
||||||
translation_placeholders={
|
|
||||||
"domain": DOMAIN,
|
|
||||||
"integration_title": "Smarty",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
HOMEASSISTANT_DOMAIN,
|
|
||||||
f"deprecated_yaml_{DOMAIN}",
|
|
||||||
breaks_in_ha_version="2025.5.0",
|
|
||||||
is_fixable=False,
|
|
||||||
issue_domain=DOMAIN,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="deprecated_yaml",
|
|
||||||
translation_placeholders={
|
|
||||||
"domain": DOMAIN,
|
|
||||||
"integration_title": "Smarty",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool:
|
||||||
"""Set up the Smarty environment from a config entry."""
|
"""Set up the Smarty environment from a config entry."""
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from pysmarty2 import Smarty
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -50,17 +50,3 @@ class SmartyConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(
|
|
||||||
self, import_config: dict[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle a flow initialized by import."""
|
|
||||||
error = await self.hass.async_add_executor_job(
|
|
||||||
self._test_connection, import_config[CONF_HOST]
|
|
||||||
)
|
|
||||||
if not error:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=import_config[CONF_NAME],
|
|
||||||
data={CONF_HOST: import_config[CONF_HOST]},
|
|
||||||
)
|
|
||||||
return self.async_abort(reason=error)
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import SmartyCoordinator
|
from .coordinator import SmartyCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,20 +20,6 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
|
||||||
"deprecated_yaml_import_issue_unknown": {
|
|
||||||
"title": "YAML import failed with unknown error",
|
|
||||||
"description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
|
||||||
},
|
|
||||||
"deprecated_yaml_import_issue_auth_error": {
|
|
||||||
"title": "YAML import failed due to an authentication error",
|
|
||||||
"description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
|
||||||
},
|
|
||||||
"deprecated_yaml_import_issue_cannot_connect": {
|
|
||||||
"title": "YAML import failed due to a connection error",
|
|
||||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"alarm": {
|
"alarm": {
|
||||||
|
@ -199,7 +199,7 @@
|
|||||||
"name": "Charge limit"
|
"name": "Charge limit"
|
||||||
},
|
},
|
||||||
"off_grid_vehicle_charging_reserve_percent": {
|
"off_grid_vehicle_charging_reserve_percent": {
|
||||||
"name": "Off grid reserve"
|
"name": "Off-grid reserve"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"connected": "Vehicle connected",
|
"connected": "Vehicle connected",
|
||||||
"ready": "Ready to charge",
|
"ready": "Ready to charge",
|
||||||
"negotiating": "Negotiating connection",
|
"negotiating": "Negotiating connection",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"charging_finished": "Charging finished",
|
"charging_finished": "Charging finished",
|
||||||
"waiting_car": "Waiting for car",
|
"waiting_car": "Waiting for car",
|
||||||
"charging_reduced": "Charging (reduced)",
|
"charging_reduced": "Charging (reduced)",
|
||||||
|
@ -363,7 +363,7 @@
|
|||||||
"name": "Charge limit"
|
"name": "Charge limit"
|
||||||
},
|
},
|
||||||
"off_grid_vehicle_charging_reserve_percent": {
|
"off_grid_vehicle_charging_reserve_percent": {
|
||||||
"name": "Off grid reserve"
|
"name": "Off-grid reserve"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cover": {
|
"cover": {
|
||||||
@ -495,10 +495,10 @@
|
|||||||
"name": "Island status",
|
"name": "Island status",
|
||||||
"state": {
|
"state": {
|
||||||
"island_status_unknown": "Unknown",
|
"island_status_unknown": "Unknown",
|
||||||
"on_grid": "On grid",
|
"on_grid": "On-grid",
|
||||||
"off_grid": "Off grid",
|
"off_grid": "Off-grid",
|
||||||
"off_grid_intentional": "Off grid intentional",
|
"off_grid_intentional": "Off-grid intentional",
|
||||||
"off_grid_unintentional": "Off grid unintentional"
|
"off_grid_unintentional": "Off-grid unintentional"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"load_power": {
|
"load_power": {
|
||||||
@ -662,7 +662,7 @@
|
|||||||
"message": "Departure time required to enable preconditioning"
|
"message": "Departure time required to enable preconditioning"
|
||||||
},
|
},
|
||||||
"set_scheduled_departure_off_peak": {
|
"set_scheduled_departure_off_peak": {
|
||||||
"message": "To enable scheduled departure, end off peak time is required."
|
"message": "To enable scheduled departure, 'End off-peak time' is required."
|
||||||
},
|
},
|
||||||
"invalid_device": {
|
"invalid_device": {
|
||||||
"message": "Invalid device ID: {device_id}"
|
"message": "Invalid device ID: {device_id}"
|
||||||
@ -752,15 +752,15 @@
|
|||||||
},
|
},
|
||||||
"end_off_peak_time": {
|
"end_off_peak_time": {
|
||||||
"description": "Time to complete charging by.",
|
"description": "Time to complete charging by.",
|
||||||
"name": "End off peak time"
|
"name": "End off-peak time"
|
||||||
},
|
},
|
||||||
"off_peak_charging_enabled": {
|
"off_peak_charging_enabled": {
|
||||||
"description": "Enable off peak charging.",
|
"description": "Enable off-peak charging.",
|
||||||
"name": "Off peak charging enabled"
|
"name": "Off-peak charging enabled"
|
||||||
},
|
},
|
||||||
"off_peak_charging_weekdays_only": {
|
"off_peak_charging_weekdays_only": {
|
||||||
"description": "Enable off peak charging on weekdays only.",
|
"description": "Enable off-peak charging on weekdays only.",
|
||||||
"name": "Off peak charging weekdays only"
|
"name": "Off-peak charging weekdays only"
|
||||||
},
|
},
|
||||||
"preconditioning_enabled": {
|
"preconditioning_enabled": {
|
||||||
"description": "Enable preconditioning.",
|
"description": "Enable preconditioning.",
|
||||||
|
@ -217,7 +217,7 @@
|
|||||||
"connected": "[%key:common::state::connected%]",
|
"connected": "[%key:common::state::connected%]",
|
||||||
"scheduled": "Scheduled",
|
"scheduled": "Scheduled",
|
||||||
"negotiating": "Negotiating",
|
"negotiating": "Negotiating",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"charging_finished": "Charging finished",
|
"charging_finished": "Charging finished",
|
||||||
"waiting_car": "Waiting car",
|
"waiting_car": "Waiting car",
|
||||||
"charging_reduced": "Charging reduced"
|
"charging_reduced": "Charging reduced"
|
||||||
@ -495,7 +495,7 @@
|
|||||||
"name": "Speed limit"
|
"name": "Speed limit"
|
||||||
},
|
},
|
||||||
"off_grid_vehicle_charging_reserve_percent": {
|
"off_grid_vehicle_charging_reserve_percent": {
|
||||||
"name": "Off grid reserve"
|
"name": "Off-grid reserve"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
@ -24,14 +24,14 @@
|
|||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
},
|
||||||
"title": "Update TP-Link Omada Credentials",
|
"title": "Update TP-Link Omada credentials",
|
||||||
"description": "The provided credentials have stopped working. Please update them."
|
"description": "The provided credentials have stopped working. Please update them."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"unsupported_controller": "Omada Controller version not supported.",
|
"unsupported_controller": "Omada controller version not supported.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
"no_sites_found": "No sites found which the user can manage."
|
"no_sites_found": "No sites found which the user can manage."
|
||||||
},
|
},
|
||||||
@ -46,31 +46,31 @@
|
|||||||
"name": "Port {port_name} PoE"
|
"name": "Port {port_name} PoE"
|
||||||
},
|
},
|
||||||
"wan_connect_ipv4": {
|
"wan_connect_ipv4": {
|
||||||
"name": "Port {port_name} Internet Connected"
|
"name": "Port {port_name} Internet connected"
|
||||||
},
|
},
|
||||||
"wan_connect_ipv6": {
|
"wan_connect_ipv6": {
|
||||||
"name": "Port {port_name} Internet Connected (IPv6)"
|
"name": "Port {port_name} Internet connected (IPv6)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"wan_link": {
|
"wan_link": {
|
||||||
"name": "Port {port_name} Internet Link"
|
"name": "Port {port_name} Internet link"
|
||||||
},
|
},
|
||||||
"online_detection": {
|
"online_detection": {
|
||||||
"name": "Port {port_name} Online Detection"
|
"name": "Port {port_name} online detection"
|
||||||
},
|
},
|
||||||
"lan_status": {
|
"lan_status": {
|
||||||
"name": "Port {port_name} LAN Status"
|
"name": "Port {port_name} LAN status"
|
||||||
},
|
},
|
||||||
"poe_delivery": {
|
"poe_delivery": {
|
||||||
"name": "Port {port_name} PoE Delivery"
|
"name": "Port {port_name} PoE delivery"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"device_status": {
|
"device_status": {
|
||||||
"name": "Device status",
|
"name": "Device status",
|
||||||
"state": {
|
"state": {
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"disconnected": "[%key:common::state::disconnected%]",
|
"disconnected": "[%key:common::state::disconnected%]",
|
||||||
"connected": "[%key:common::state::connected%]",
|
"connected": "[%key:common::state::connected%]",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"reconnect_client": {
|
"reconnect_client": {
|
||||||
"name": "Reconnect wireless client",
|
"name": "Reconnect wireless client",
|
||||||
"description": "Tries to get wireless client to reconnect to Omada Network.",
|
"description": "Tries to get wireless client to reconnect to Omada network.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"mac": {
|
"mac": {
|
||||||
"name": "MAC address",
|
"name": "MAC address",
|
||||||
|
@ -6,5 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/uptimerobot",
|
"documentation": "https://www.home-assistant.io/integrations/uptimerobot",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyuptimerobot"],
|
"loggers": ["pyuptimerobot"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyuptimerobot==22.2.0"]
|
"requirements": ["pyuptimerobot==22.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,7 @@ rules:
|
|||||||
appropriate-polling: done
|
appropriate-polling: done
|
||||||
brands: done
|
brands: done
|
||||||
common-modules: done
|
common-modules: done
|
||||||
config-flow-test-coverage:
|
config-flow-test-coverage: done
|
||||||
status: todo
|
|
||||||
comment: fix name and docstring
|
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
docs-actions:
|
docs-actions:
|
||||||
@ -41,9 +39,7 @@ rules:
|
|||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: done
|
||||||
test-coverage:
|
test-coverage: done
|
||||||
status: todo
|
|
||||||
comment: recheck typos
|
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
|
@ -577,10 +577,10 @@ class UtilityMeterSensor(RestoreSensor):
|
|||||||
async def _async_reset_meter(self, event):
|
async def _async_reset_meter(self, event):
|
||||||
"""Reset the utility meter status."""
|
"""Reset the utility meter status."""
|
||||||
|
|
||||||
await self._program_reset()
|
|
||||||
|
|
||||||
await self.async_reset_meter(self._tariff_entity)
|
await self.async_reset_meter(self._tariff_entity)
|
||||||
|
|
||||||
|
await self._program_reset()
|
||||||
|
|
||||||
async def async_reset_meter(self, entity_id):
|
async def async_reset_meter(self, entity_id):
|
||||||
"""Reset meter."""
|
"""Reset meter."""
|
||||||
if self._tariff_entity is not None and self._tariff_entity != entity_id:
|
if self._tariff_entity is not None and self._tariff_entity != entity_id:
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"cleaning": "Cleaning",
|
"cleaning": "Cleaning",
|
||||||
"docked": "Docked",
|
"docked": "Docked",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"idle": "[%key:common::state::idle%]",
|
"idle": "[%key:common::state::idle%]",
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import VodafoneConfigEntry, VodafoneStationRouter
|
from .coordinator import VodafoneConfigEntry, VodafoneStationRouter
|
||||||
|
|
||||||
PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR]
|
PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR]
|
||||||
@ -36,7 +35,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: VodafoneConfigEntry) ->
|
|||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
await coordinator.api.logout()
|
await coordinator.api.logout()
|
||||||
await coordinator.api.close()
|
await coordinator.api.close()
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
@ -1509,7 +1509,7 @@
|
|||||||
"name": "Software error",
|
"name": "Software error",
|
||||||
"state": {
|
"state": {
|
||||||
"nothing": "Good",
|
"nothing": "Good",
|
||||||
"something": "Error"
|
"something": "[%key:common::state::error%]"
|
||||||
},
|
},
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"top_pcb_sensor_error": {
|
"top_pcb_sensor_error": {
|
||||||
|
@ -21,7 +21,7 @@ from socket import ( # type: ignore[attr-defined] # private, not in typeshed
|
|||||||
_GLOBAL_DEFAULT_TIMEOUT,
|
_GLOBAL_DEFAULT_TIMEOUT,
|
||||||
)
|
)
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, cast, overload
|
from typing import TYPE_CHECKING, Any, cast, overload
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -355,7 +355,13 @@ def ensure_list[_T](value: _T | None) -> list[_T] | list[Any]:
|
|||||||
"""Wrap value in list if it is not one."""
|
"""Wrap value in list if it is not one."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return []
|
return []
|
||||||
return cast(list[_T], value) if isinstance(value, list) else [value]
|
if isinstance(value, list):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# https://github.com/home-assistant/core/pull/71960
|
||||||
|
# cast with a type variable is still slow.
|
||||||
|
return cast(list[_T], value)
|
||||||
|
return value # type: ignore[unreachable]
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
|
||||||
def entity_id(value: Any) -> str:
|
def entity_id(value: Any) -> str:
|
||||||
|
@ -6,7 +6,7 @@ aiodns==3.2.0
|
|||||||
aiohasupervisor==0.3.1b1
|
aiohasupervisor==0.3.1b1
|
||||||
aiohttp-asyncmdnsresolver==0.1.1
|
aiohttp-asyncmdnsresolver==0.1.1
|
||||||
aiohttp-fast-zlib==0.2.3
|
aiohttp-fast-zlib==0.2.3
|
||||||
aiohttp==3.11.16
|
aiohttp==3.11.18
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
aiousbwatcher==1.1.1
|
aiousbwatcher==1.1.1
|
||||||
aiozoneinfo==0.2.3
|
aiozoneinfo==0.2.3
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools==77.0.3"]
|
requires = ["setuptools==78.1.1"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
@ -28,7 +28,7 @@ dependencies = [
|
|||||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||||
# Lib can be removed with 2025.11
|
# Lib can be removed with 2025.11
|
||||||
"aiohasupervisor==0.3.1b1",
|
"aiohasupervisor==0.3.1b1",
|
||||||
"aiohttp==3.11.16",
|
"aiohttp==3.11.18",
|
||||||
"aiohttp_cors==0.7.0",
|
"aiohttp_cors==0.7.0",
|
||||||
"aiohttp-fast-zlib==0.2.3",
|
"aiohttp-fast-zlib==0.2.3",
|
||||||
"aiohttp-asyncmdnsresolver==0.1.1",
|
"aiohttp-asyncmdnsresolver==0.1.1",
|
||||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@ -5,7 +5,7 @@
|
|||||||
# Home Assistant Core
|
# Home Assistant Core
|
||||||
aiodns==3.2.0
|
aiodns==3.2.0
|
||||||
aiohasupervisor==0.3.1b1
|
aiohasupervisor==0.3.1b1
|
||||||
aiohttp==3.11.16
|
aiohttp==3.11.18
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
aiohttp-fast-zlib==0.2.3
|
aiohttp-fast-zlib==0.2.3
|
||||||
aiohttp-asyncmdnsresolver==0.1.1
|
aiohttp-asyncmdnsresolver==0.1.1
|
||||||
|
14
requirements_all.txt
generated
14
requirements_all.txt
generated
@ -179,7 +179,7 @@ aioacaia==0.1.14
|
|||||||
aioairq==0.4.4
|
aioairq==0.4.4
|
||||||
|
|
||||||
# homeassistant.components.airzone_cloud
|
# homeassistant.components.airzone_cloud
|
||||||
aioairzone-cloud==0.6.11
|
aioairzone-cloud==0.6.12
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==1.0.0
|
aioairzone==1.0.0
|
||||||
@ -371,7 +371,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==13.4.1
|
aioshelly==13.5.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -1172,7 +1172,7 @@ horimote==0.4.1
|
|||||||
httplib2==0.20.4
|
httplib2==0.20.4
|
||||||
|
|
||||||
# homeassistant.components.huawei_lte
|
# homeassistant.components.huawei_lte
|
||||||
huawei-lte-api==1.10.0
|
huawei-lte-api==1.11.0
|
||||||
|
|
||||||
# homeassistant.components.huum
|
# homeassistant.components.huum
|
||||||
huum==0.7.12
|
huum==0.7.12
|
||||||
@ -2134,7 +2134,7 @@ pymeteoclimatic==0.1.0
|
|||||||
pymicro-vad==1.0.1
|
pymicro-vad==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.miele
|
# homeassistant.components.miele
|
||||||
pymiele==0.3.4
|
pymiele==0.3.6
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_tv
|
# homeassistant.components.xiaomi_tv
|
||||||
pymitv==1.4.3
|
pymitv==1.4.3
|
||||||
@ -2214,7 +2214,7 @@ pyotgw==2.2.2
|
|||||||
pyotp==2.8.0
|
pyotp==2.8.0
|
||||||
|
|
||||||
# homeassistant.components.overkiz
|
# homeassistant.components.overkiz
|
||||||
pyoverkiz==1.17.0
|
pyoverkiz==1.17.1
|
||||||
|
|
||||||
# homeassistant.components.onewire
|
# homeassistant.components.onewire
|
||||||
pyownet==0.10.0.post1
|
pyownet==0.10.0.post1
|
||||||
@ -2289,7 +2289,7 @@ pysabnzbd==1.1.1
|
|||||||
pysaj==0.0.16
|
pysaj==0.0.16
|
||||||
|
|
||||||
# homeassistant.components.schlage
|
# homeassistant.components.schlage
|
||||||
pyschlage==2024.11.0
|
pyschlage==2025.4.0
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.1.0
|
pysensibo==1.1.0
|
||||||
@ -2436,7 +2436,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.10.2
|
python-kasa[speedups]==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.2.3
|
python-linkplay==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.lirc
|
# homeassistant.components.lirc
|
||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
|
14
requirements_test_all.txt
generated
14
requirements_test_all.txt
generated
@ -167,7 +167,7 @@ aioacaia==0.1.14
|
|||||||
aioairq==0.4.4
|
aioairq==0.4.4
|
||||||
|
|
||||||
# homeassistant.components.airzone_cloud
|
# homeassistant.components.airzone_cloud
|
||||||
aioairzone-cloud==0.6.11
|
aioairzone-cloud==0.6.12
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==1.0.0
|
aioairzone==1.0.0
|
||||||
@ -353,7 +353,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==13.4.1
|
aioshelly==13.5.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -999,7 +999,7 @@ homematicip==2.0.0
|
|||||||
httplib2==0.20.4
|
httplib2==0.20.4
|
||||||
|
|
||||||
# homeassistant.components.huawei_lte
|
# homeassistant.components.huawei_lte
|
||||||
huawei-lte-api==1.10.0
|
huawei-lte-api==1.11.0
|
||||||
|
|
||||||
# homeassistant.components.huum
|
# homeassistant.components.huum
|
||||||
huum==0.7.12
|
huum==0.7.12
|
||||||
@ -1746,7 +1746,7 @@ pymeteoclimatic==0.1.0
|
|||||||
pymicro-vad==1.0.1
|
pymicro-vad==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.miele
|
# homeassistant.components.miele
|
||||||
pymiele==0.3.4
|
pymiele==0.3.6
|
||||||
|
|
||||||
# homeassistant.components.mochad
|
# homeassistant.components.mochad
|
||||||
pymochad==0.2.0
|
pymochad==0.2.0
|
||||||
@ -1811,7 +1811,7 @@ pyotgw==2.2.2
|
|||||||
pyotp==2.8.0
|
pyotp==2.8.0
|
||||||
|
|
||||||
# homeassistant.components.overkiz
|
# homeassistant.components.overkiz
|
||||||
pyoverkiz==1.17.0
|
pyoverkiz==1.17.1
|
||||||
|
|
||||||
# homeassistant.components.onewire
|
# homeassistant.components.onewire
|
||||||
pyownet==0.10.0.post1
|
pyownet==0.10.0.post1
|
||||||
@ -1871,7 +1871,7 @@ pyrympro==0.0.9
|
|||||||
pysabnzbd==1.1.1
|
pysabnzbd==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.schlage
|
# homeassistant.components.schlage
|
||||||
pyschlage==2024.11.0
|
pyschlage==2025.4.0
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.1.0
|
pysensibo==1.1.0
|
||||||
@ -1976,7 +1976,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.10.2
|
python-kasa[speedups]==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.2.3
|
python-linkplay==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.matter
|
# homeassistant.components.matter
|
||||||
python-matter-server==7.0.0
|
python-matter-server==7.0.0
|
||||||
|
@ -2131,7 +2131,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
|||||||
"upcloud",
|
"upcloud",
|
||||||
"upnp",
|
"upnp",
|
||||||
"uptime",
|
"uptime",
|
||||||
"uptimerobot",
|
|
||||||
"usb",
|
"usb",
|
||||||
"usgs_earthquakes_feed",
|
"usgs_earthquakes_feed",
|
||||||
"utility_meter",
|
"utility_meter",
|
||||||
|
@ -191,7 +191,6 @@ EXCEPTIONS = {
|
|||||||
"enocean", # https://github.com/kipe/enocean/pull/142
|
"enocean", # https://github.com/kipe/enocean/pull/142
|
||||||
"imutils", # https://github.com/PyImageSearch/imutils/pull/292
|
"imutils", # https://github.com/PyImageSearch/imutils/pull/292
|
||||||
"iso4217", # Public domain
|
"iso4217", # Public domain
|
||||||
"jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21
|
|
||||||
"kiwiki_client", # https://github.com/c7h/kiwiki_client/pull/6
|
"kiwiki_client", # https://github.com/c7h/kiwiki_client/pull/6
|
||||||
"ld2410-ble", # https://github.com/930913/ld2410-ble/pull/7
|
"ld2410-ble", # https://github.com/930913/ld2410-ble/pull/7
|
||||||
"maxcube-api", # https://github.com/uebelack/python-maxcube-api/pull/48
|
"maxcube-api", # https://github.com/uebelack/python-maxcube-api/pull/48
|
||||||
@ -205,6 +204,11 @@ EXCEPTIONS = {
|
|||||||
"repoze.lru",
|
"repoze.lru",
|
||||||
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
|
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
|
||||||
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
|
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
|
||||||
|
# ---
|
||||||
|
# https://github.com/jaraco/skeleton/pull/170
|
||||||
|
# https://github.com/jaraco/skeleton/pull/171
|
||||||
|
"jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21
|
||||||
|
"setuptools", # MIT
|
||||||
}
|
}
|
||||||
|
|
||||||
TODO = {
|
TODO = {
|
||||||
|
@ -46,6 +46,11 @@ from homeassistant.components import device_automation, persistent_notification
|
|||||||
from homeassistant.components.device_automation import ( # noqa: F401
|
from homeassistant.components.device_automation import ( # noqa: F401
|
||||||
_async_get_device_automation_capabilities as async_get_device_automation_capabilities,
|
_async_get_device_automation_capabilities as async_get_device_automation_capabilities,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.logger import (
|
||||||
|
DOMAIN as LOGGER_DOMAIN,
|
||||||
|
SERVICE_SET_LEVEL,
|
||||||
|
_clear_logger_overwrites,
|
||||||
|
)
|
||||||
from homeassistant.config import IntegrationConfigInfo, async_process_component_config
|
from homeassistant.config import IntegrationConfigInfo, async_process_component_config
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -1688,6 +1693,28 @@ def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) ->
|
|||||||
async_dispatcher_send(hass, SIGNAL_CLOUD_CONNECTION_STATE, state)
|
async_dispatcher_send(hass, SIGNAL_CLOUD_CONNECTION_STATE, state)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def async_call_logger_set_level(
|
||||||
|
logger: str,
|
||||||
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "FATAL", "CRITICAL"],
|
||||||
|
*,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> AsyncGenerator[None]:
|
||||||
|
"""Context manager to reset loggers after logger.set_level call."""
|
||||||
|
assert LOGGER_DOMAIN in hass.data, "'logger' integration not setup"
|
||||||
|
with caplog.at_level(logging.NOTSET, logger):
|
||||||
|
await hass.services.async_call(
|
||||||
|
LOGGER_DOMAIN,
|
||||||
|
SERVICE_SET_LEVEL,
|
||||||
|
{logger: level},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
yield
|
||||||
|
_clear_logger_overwrites(hass)
|
||||||
|
|
||||||
|
|
||||||
def import_and_test_deprecated_constant_enum(
|
def import_and_test_deprecated_constant_enum(
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
module: ModuleType,
|
module: ModuleType,
|
||||||
|
@ -120,7 +120,7 @@
|
|||||||
}),
|
}),
|
||||||
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
|
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
|
||||||
'original_icon': None,
|
'original_icon': None,
|
||||||
'original_name': 'Off grid status',
|
'original_name': 'Off-grid status',
|
||||||
'platform': 'apsystems',
|
'platform': 'apsystems',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'supported_features': 0,
|
'supported_features': 0,
|
||||||
@ -133,7 +133,7 @@
|
|||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'device_class': 'problem',
|
'device_class': 'problem',
|
||||||
'friendly_name': 'Mock Title Off grid status',
|
'friendly_name': 'Mock Title Off-grid status',
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'binary_sensor.mock_title_off_grid_status',
|
'entity_id': 'binary_sensor.mock_title_off_grid_status',
|
||||||
|
@ -61,6 +61,7 @@ from . import (
|
|||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
MockModule,
|
MockModule,
|
||||||
|
async_call_logger_set_level,
|
||||||
async_fire_time_changed,
|
async_fire_time_changed,
|
||||||
load_fixture,
|
load_fixture,
|
||||||
mock_integration,
|
mock_integration,
|
||||||
@ -1144,14 +1145,9 @@ async def test_debug_logging(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test debug logging."""
|
"""Test debug logging."""
|
||||||
assert await async_setup_component(hass, "logger", {"logger": {}})
|
assert await async_setup_component(hass, "logger", {"logger": {}})
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.bluetooth", "DEBUG", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.bluetooth": "DEBUG"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
address = "44:44:33:11:23:41"
|
address = "44:44:33:11:23:41"
|
||||||
start_time_monotonic = 50.0
|
start_time_monotonic = 50.0
|
||||||
|
|
||||||
@ -1171,13 +1167,9 @@ async def test_debug_logging(
|
|||||||
assert "wohand_poor_signal_hci0" in caplog.text
|
assert "wohand_poor_signal_hci0" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.bluetooth", "WARNING", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.bluetooth": "WARNING"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
switchbot_device_good_signal_hci0 = generate_ble_device(
|
switchbot_device_good_signal_hci0 = generate_ble_device(
|
||||||
address, "wohand_good_signal_hci0"
|
address, "wohand_good_signal_hci0"
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,11 @@ from . import (
|
|||||||
patch_bluetooth_time,
|
patch_bluetooth_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_call_logger_set_level,
|
||||||
|
async_fire_time_changed,
|
||||||
|
)
|
||||||
|
|
||||||
# If the adapter is in a stuck state the following errors are raised:
|
# If the adapter is in a stuck state the following errors are raised:
|
||||||
NEED_RESET_ERRORS = [
|
NEED_RESET_ERRORS = [
|
||||||
@ -482,12 +486,9 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test we can recover the adapter at startup and we wait for Dbus to init."""
|
"""Test we can recover the adapter at startup and we wait for Dbus to init."""
|
||||||
assert await async_setup_component(hass, "logger", {})
|
assert await async_setup_component(hass, "logger", {})
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.bluetooth", "DEBUG", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.bluetooth": "DEBUG"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
called_start = 0
|
called_start = 0
|
||||||
called_stop = 0
|
called_stop = 0
|
||||||
_callback = None
|
_callback = None
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test the DHCP discovery integration."""
|
"""Test the DHCP discovery integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
import threading
|
||||||
@ -24,6 +26,7 @@ from homeassistant.components.device_tracker import (
|
|||||||
SourceType,
|
SourceType,
|
||||||
)
|
)
|
||||||
from homeassistant.components.dhcp.const import DOMAIN
|
from homeassistant.components.dhcp.const import DOMAIN
|
||||||
|
from homeassistant.components.dhcp.models import DHCPData
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
@ -147,12 +150,12 @@ async def _async_get_handle_dhcp_packet(
|
|||||||
integration_matchers: dhcp.DhcpMatchers,
|
integration_matchers: dhcp.DhcpMatchers,
|
||||||
address_data: dict | None = None,
|
address_data: dict | None = None,
|
||||||
) -> Callable[[Any], Awaitable[None]]:
|
) -> Callable[[Any], Awaitable[None]]:
|
||||||
|
"""Make a handler for a dhcp packet."""
|
||||||
if address_data is None:
|
if address_data is None:
|
||||||
address_data = {}
|
address_data = {}
|
||||||
dhcp_watcher = dhcp.DHCPWatcher(
|
dhcp_watcher = dhcp.DHCPWatcher(
|
||||||
hass,
|
hass,
|
||||||
address_data,
|
DHCPData(integration_matchers, set(), address_data),
|
||||||
integration_matchers,
|
|
||||||
)
|
)
|
||||||
with patch("aiodhcpwatcher.async_start"):
|
with patch("aiodhcpwatcher.async_start"):
|
||||||
await dhcp_watcher.async_start()
|
await dhcp_watcher.async_start()
|
||||||
@ -666,6 +669,45 @@ async def test_setup_fails_with_broken_libpcap(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_device_tracker_watcher(
|
||||||
|
hass: HomeAssistant, matchers: list[dhcp.DHCPMatcher]
|
||||||
|
) -> dhcp.DeviceTrackerWatcher:
|
||||||
|
return dhcp.DeviceTrackerWatcher(
|
||||||
|
hass,
|
||||||
|
DHCPData(
|
||||||
|
dhcp.async_index_integration_matchers(matchers),
|
||||||
|
set(),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_device_tracker_registered_watcher(
|
||||||
|
hass: HomeAssistant, matchers: list[dhcp.DHCPMatcher]
|
||||||
|
) -> dhcp.DeviceTrackerRegisteredWatcher:
|
||||||
|
return dhcp.DeviceTrackerRegisteredWatcher(
|
||||||
|
hass,
|
||||||
|
DHCPData(
|
||||||
|
dhcp.async_index_integration_matchers(matchers),
|
||||||
|
set(),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_network_watcher(
|
||||||
|
hass: HomeAssistant, matchers: list[dhcp.DHCPMatcher]
|
||||||
|
) -> dhcp.NetworkWatcher:
|
||||||
|
return dhcp.NetworkWatcher(
|
||||||
|
hass,
|
||||||
|
DHCPData(
|
||||||
|
dhcp.async_index_integration_matchers(matchers),
|
||||||
|
set(),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_device_tracker_hostname_and_macaddress_exists_before_start(
|
async def test_device_tracker_hostname_and_macaddress_exists_before_start(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -682,18 +724,15 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -716,18 +755,15 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(
|
|||||||
async def test_device_tracker_registered(hass: HomeAssistant) -> None:
|
async def test_device_tracker_registered(hass: HomeAssistant) -> None:
|
||||||
"""Test matching based on hostname and macaddress when registered."""
|
"""Test matching based on hostname and macaddress when registered."""
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher(
|
device_tracker_watcher = _make_device_tracker_registered_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -756,18 +792,15 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None:
|
|||||||
async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> None:
|
async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> None:
|
||||||
"""Test handle None hostname."""
|
"""Test handle None hostname."""
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -789,18 +822,15 @@ async def test_device_tracker_hostname_and_macaddress_after_start(
|
|||||||
"""Test matching based on hostname and macaddress after start."""
|
"""Test matching based on hostname and macaddress after start."""
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -837,18 +867,15 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home(
|
|||||||
"""Test matching based on hostname and macaddress after start but not home."""
|
"""Test matching based on hostname and macaddress after start but not home."""
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -875,9 +902,8 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_router(
|
|||||||
"""Test matching based on hostname and macaddress after start but not router."""
|
"""Test matching based on hostname and macaddress after start but not router."""
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
@ -905,9 +931,8 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi
|
|||||||
"""Test matching based on hostname and macaddress after start but missing hostname."""
|
"""Test matching based on hostname and macaddress after start but missing hostname."""
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
@ -934,9 +959,8 @@ async def test_device_tracker_invalid_ip_address(
|
|||||||
"""Test an invalid ip address."""
|
"""Test an invalid ip address."""
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
@ -974,18 +998,15 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
|
device_tracker_watcher = _make_device_tracker_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1010,18 +1031,15 @@ async def test_aiodiscover_finds_new_hosts(hass: HomeAssistant) -> None:
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
device_tracker_watcher = dhcp.NetworkWatcher(
|
device_tracker_watcher = _make_network_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1073,18 +1091,15 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
device_tracker_watcher = dhcp.NetworkWatcher(
|
device_tracker_watcher = _make_network_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "irobot-*",
|
"hostname": "irobot-*",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1123,19 +1138,17 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) -
|
|||||||
return_value=[],
|
return_value=[],
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
device_tracker_watcher = dhcp.NetworkWatcher(
|
device_tracker_watcher = _make_network_watcher(
|
||||||
hass,
|
hass,
|
||||||
{},
|
|
||||||
dhcp.async_index_integration_matchers(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": "mock-domain",
|
"domain": "mock-domain",
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "B8B7F1*",
|
"macaddress": "B8B7F1*",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -1235,7 +1248,7 @@ async def test_dhcp_rediscover(
|
|||||||
hass, integration_matchers, address_data
|
hass, integration_matchers, address_data
|
||||||
)
|
)
|
||||||
rediscovery_watcher = dhcp.RediscoveryWatcher(
|
rediscovery_watcher = dhcp.RediscoveryWatcher(
|
||||||
hass, address_data, integration_matchers
|
hass, DHCPData(integration_matchers, set(), address_data)
|
||||||
)
|
)
|
||||||
rediscovery_watcher.async_start()
|
rediscovery_watcher.async_start()
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
@ -1329,7 +1342,7 @@ async def test_dhcp_rediscover_no_match(
|
|||||||
hass, integration_matchers, address_data
|
hass, integration_matchers, address_data
|
||||||
)
|
)
|
||||||
rediscovery_watcher = dhcp.RediscoveryWatcher(
|
rediscovery_watcher = dhcp.RediscoveryWatcher(
|
||||||
hass, address_data, integration_matchers
|
hass, DHCPData(integration_matchers, set(), address_data)
|
||||||
)
|
)
|
||||||
rediscovery_watcher.async_start()
|
rediscovery_watcher.async_start()
|
||||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||||
|
75
tests/components/dhcp/test_websocket_api.py
Normal file
75
tests/components/dhcp/test_websocket_api.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""The tests for the dhcp WebSocket API."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import aiodhcpwatcher
|
||||||
|
|
||||||
|
from homeassistant.components.dhcp import DOMAIN
|
||||||
|
from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_subscribe_discovery(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test dhcp subscribe_discovery."""
|
||||||
|
saved_callback: Callable[[aiodhcpwatcher.DHCPRequest], None] | None = None
|
||||||
|
|
||||||
|
async def mock_start(
|
||||||
|
callback: Callable[[aiodhcpwatcher.DHCPRequest], None],
|
||||||
|
) -> None:
|
||||||
|
"""Mock start."""
|
||||||
|
nonlocal saved_callback
|
||||||
|
saved_callback = callback
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.dhcp.aiodhcpwatcher.async_start", mock_start),
|
||||||
|
patch("homeassistant.components.dhcp.DiscoverHosts"),
|
||||||
|
):
|
||||||
|
await async_setup_component(hass, DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
saved_callback(aiodhcpwatcher.DHCPRequest("4.3.2.2", "happy", "44:44:33:11:23:12"))
|
||||||
|
client = await hass_ws_client()
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "dhcp/subscribe_discovery",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async with asyncio.timeout(1):
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
async with asyncio.timeout(1):
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {
|
||||||
|
"add": [
|
||||||
|
{
|
||||||
|
"hostname": "happy",
|
||||||
|
"ip_address": "4.3.2.2",
|
||||||
|
"mac_address": "44:44:33:11:23:12",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
saved_callback(aiodhcpwatcher.DHCPRequest("4.3.2.1", "sad", "44:44:33:11:23:13"))
|
||||||
|
|
||||||
|
async with asyncio.timeout(1):
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {
|
||||||
|
"add": [
|
||||||
|
{
|
||||||
|
"hostname": "sad",
|
||||||
|
"ip_address": "4.3.2.1",
|
||||||
|
"mac_address": "44:44:33:11:23:13",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -27,6 +27,7 @@ from homeassistant.components.esphome.const import (
|
|||||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
@ -50,6 +51,17 @@ def mock_setup_entry():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def get_flow_context(hass: HomeAssistant, result: ConfigFlowResult) -> dict[str, Any]:
|
||||||
|
"""Get the flow context from the result of async_init or async_configure."""
|
||||||
|
flow = next(
|
||||||
|
flow
|
||||||
|
for flow in hass.config_entries.flow.async_progress()
|
||||||
|
if flow["flow_id"] == result["flow_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return flow["context"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_zeroconf")
|
@pytest.mark.usefixtures("mock_zeroconf")
|
||||||
async def test_user_connection_works(
|
async def test_user_connection_works(
|
||||||
hass: HomeAssistant, mock_client, mock_setup_entry: None
|
hass: HomeAssistant, mock_client, mock_setup_entry: None
|
||||||
@ -119,7 +131,12 @@ async def test_user_connection_updates_host(
|
|||||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
|
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
assert entry.data[CONF_HOST] == "127.0.0.1"
|
assert entry.data[CONF_HOST] == "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
@ -145,6 +162,9 @@ async def test_user_sets_unique_id(
|
|||||||
|
|
||||||
assert discovery_result["type"] is FlowResultType.FORM
|
assert discovery_result["type"] is FlowResultType.FORM
|
||||||
assert discovery_result["step_id"] == "discovery_confirm"
|
assert discovery_result["step_id"] == "discovery_confirm"
|
||||||
|
assert discovery_result["description_placeholders"] == {
|
||||||
|
"name": "test8266",
|
||||||
|
}
|
||||||
|
|
||||||
discovery_result = await hass.config_entries.flow.async_configure(
|
discovery_result = await hass.config_entries.flow.async_configure(
|
||||||
discovery_result["flow_id"],
|
discovery_result["flow_id"],
|
||||||
@ -173,7 +193,12 @@ async def test_user_sets_unique_id(
|
|||||||
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
|
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "test",
|
||||||
|
"name": "test",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_zeroconf")
|
@pytest.mark.usefixtures("mock_zeroconf")
|
||||||
@ -224,6 +249,9 @@ async def test_user_causes_zeroconf_to_abort(
|
|||||||
|
|
||||||
assert discovery_result["type"] is FlowResultType.FORM
|
assert discovery_result["type"] is FlowResultType.FORM
|
||||||
assert discovery_result["step_id"] == "discovery_confirm"
|
assert discovery_result["step_id"] == "discovery_confirm"
|
||||||
|
assert discovery_result["description_placeholders"] == {
|
||||||
|
"name": "test8266",
|
||||||
|
}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
"esphome",
|
"esphome",
|
||||||
@ -287,6 +315,7 @@ async def test_user_with_password(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={CONF_PASSWORD: "password1"}
|
result["flow_id"], user_input={CONF_PASSWORD: "password1"}
|
||||||
@ -316,6 +345,7 @@ async def test_user_invalid_password(hass: HomeAssistant, mock_client) -> None:
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
mock_client.connect.side_effect = InvalidAuthAPIError
|
mock_client.connect.side_effect = InvalidAuthAPIError
|
||||||
|
|
||||||
@ -325,6 +355,7 @@ async def test_user_invalid_password(hass: HomeAssistant, mock_client) -> None:
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
assert result["errors"] == {"base": "invalid_auth"}
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
@ -338,7 +369,7 @@ async def test_user_dashboard_has_wrong_key(
|
|||||||
"""Test user step with key from dashboard that is incorrect."""
|
"""Test user step with key from dashboard that is incorrect."""
|
||||||
mock_client.device_info.side_effect = [
|
mock_client.device_info.side_effect = [
|
||||||
RequiresEncryptionAPIError,
|
RequiresEncryptionAPIError,
|
||||||
InvalidEncryptionKeyAPIError,
|
InvalidEncryptionKeyAPIError("Wrong key", "test"),
|
||||||
DeviceInfo(
|
DeviceInfo(
|
||||||
uses_password=False,
|
uses_password=False,
|
||||||
name="test",
|
name="test",
|
||||||
@ -359,6 +390,7 @@ async def test_user_dashboard_has_wrong_key(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
@ -467,6 +499,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
@ -522,6 +555,7 @@ async def test_user_discovers_name_and_dashboard_is_unavailable(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
@ -553,6 +587,7 @@ async def test_login_connection_error(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
mock_client.connect.side_effect = APIConnectionError
|
mock_client.connect.side_effect = APIConnectionError
|
||||||
|
|
||||||
@ -562,6 +597,7 @@ async def test_login_connection_error(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
assert result["errors"] == {"base": "connection_error"}
|
assert result["errors"] == {"base": "connection_error"}
|
||||||
|
|
||||||
|
|
||||||
@ -578,12 +614,18 @@ async def test_discovery_initiation(
|
|||||||
port=6053,
|
port=6053,
|
||||||
properties={
|
properties={
|
||||||
"mac": "1122334455aa",
|
"mac": "1122334455aa",
|
||||||
|
"friendly_name": "The Test",
|
||||||
},
|
},
|
||||||
type="mock_type",
|
type="mock_type",
|
||||||
)
|
)
|
||||||
flow = await hass.config_entries.flow.async_init(
|
flow = await hass.config_entries.flow.async_init(
|
||||||
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
|
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
|
||||||
)
|
)
|
||||||
|
assert get_flow_context(hass, flow) == {
|
||||||
|
"source": config_entries.SOURCE_ZEROCONF,
|
||||||
|
"title_placeholders": {"name": "The Test (test)"},
|
||||||
|
"unique_id": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
@ -645,7 +687,12 @@ async def test_discovery_already_configured(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_duplicate_data(
|
async def test_discovery_duplicate_data(
|
||||||
@ -667,6 +714,7 @@ async def test_discovery_duplicate_data(
|
|||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "discovery_confirm"
|
assert result["step_id"] == "discovery_confirm"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
"esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF}
|
"esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF}
|
||||||
@ -701,7 +749,12 @@ async def test_discovery_updates_unique_id(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
assert entry.unique_id == "11:22:33:44:55:aa"
|
assert entry.unique_id == "11:22:33:44:55:aa"
|
||||||
|
|
||||||
@ -722,6 +775,7 @@ async def test_user_requires_psk(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
assert result["description_placeholders"] == {"name": "ESPHome"}
|
||||||
|
|
||||||
assert len(mock_client.connect.mock_calls) == 2
|
assert len(mock_client.connect.mock_calls) == 2
|
||||||
assert len(mock_client.device_info.mock_calls) == 2
|
assert len(mock_client.device_info.mock_calls) == 2
|
||||||
@ -744,6 +798,7 @@ async def test_encryption_key_valid_psk(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "ESPHome"}
|
||||||
|
|
||||||
mock_client.device_info = AsyncMock(
|
mock_client.device_info = AsyncMock(
|
||||||
return_value=DeviceInfo(uses_password=False, name="test")
|
return_value=DeviceInfo(uses_password=False, name="test")
|
||||||
@ -779,6 +834,7 @@ async def test_encryption_key_invalid_psk(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "ESPHome"}
|
||||||
|
|
||||||
mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError
|
mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
@ -788,6 +844,7 @@ async def test_encryption_key_invalid_psk(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
assert result["errors"] == {"base": "invalid_psk"}
|
assert result["errors"] == {"base": "invalid_psk"}
|
||||||
|
assert result["description_placeholders"] == {"name": "ESPHome"}
|
||||||
assert mock_client.noise_psk == INVALID_NOISE_PSK
|
assert mock_client.noise_psk == INVALID_NOISE_PSK
|
||||||
|
|
||||||
|
|
||||||
@ -803,6 +860,9 @@ async def test_reauth_initiation(hass: HomeAssistant, mock_client) -> None:
|
|||||||
result = await entry.start_reauth_flow(hass)
|
result = await entry.start_reauth_flow(hass)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "reauth_confirm"
|
assert result["step_id"] == "reauth_confirm"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"name": "Mock Title (test)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_zeroconf")
|
@pytest.mark.usefixtures("mock_zeroconf")
|
||||||
@ -1005,6 +1065,9 @@ async def test_reauth_fixed_via_dashboard_at_confirm(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM, result
|
assert result["type"] is FlowResultType.FORM, result
|
||||||
assert result["step_id"] == "reauth_confirm"
|
assert result["step_id"] == "reauth_confirm"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"name": "Mock Title (test)",
|
||||||
|
}
|
||||||
|
|
||||||
mock_dashboard["configured"].append(
|
mock_dashboard["configured"].append(
|
||||||
{
|
{
|
||||||
@ -1050,6 +1113,9 @@ async def test_reauth_confirm_invalid(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "reauth_confirm"
|
assert result["step_id"] == "reauth_confirm"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"name": "Mock Title (test)",
|
||||||
|
}
|
||||||
assert result["errors"]
|
assert result["errors"]
|
||||||
assert result["errors"]["base"] == "invalid_psk"
|
assert result["errors"]["base"] == "invalid_psk"
|
||||||
|
|
||||||
@ -1088,6 +1154,9 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "reauth_confirm"
|
assert result["step_id"] == "reauth_confirm"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"name": "Mock Title (test)",
|
||||||
|
}
|
||||||
assert result["errors"]
|
assert result["errors"]
|
||||||
assert result["errors"]["base"] == "invalid_psk"
|
assert result["errors"]["base"] == "invalid_psk"
|
||||||
|
|
||||||
@ -1125,6 +1194,9 @@ async def test_reauth_encryption_key_removed(
|
|||||||
result = await entry.start_reauth_flow(hass)
|
result = await entry.start_reauth_flow(hass)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "reauth_encryption_removed_confirm"
|
assert result["step_id"] == "reauth_encryption_removed_confirm"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"name": "Mock Title (test)",
|
||||||
|
}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={}
|
result["flow_id"], user_input={}
|
||||||
@ -1159,7 +1231,12 @@ async def test_discovery_dhcp_updates_host(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.184"
|
assert entry.data[CONF_HOST] == "192.168.43.184"
|
||||||
|
|
||||||
@ -1188,7 +1265,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_detailed"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
# Mac was wrong, should not update
|
# Mac was wrong, should not update
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.183"
|
assert entry.data[CONF_HOST] == "192.168.43.183"
|
||||||
@ -1217,7 +1299,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac_bad_key(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_detailed"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
# Mac was wrong, should not update
|
# Mac was wrong, should not update
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.183"
|
assert entry.data[CONF_HOST] == "192.168.43.183"
|
||||||
@ -1246,7 +1333,12 @@ async def test_discovery_dhcp_does_not_update_host_missing_mac_bad_key(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_detailed"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "unknown",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
|
|
||||||
# Mac was missing, should not update
|
# Mac was missing, should not update
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.183"
|
assert entry.data[CONF_HOST] == "192.168.43.183"
|
||||||
@ -1330,6 +1422,7 @@ async def test_zeroconf_encryption_key_via_dashboard(
|
|||||||
|
|
||||||
assert flow["type"] is FlowResultType.FORM
|
assert flow["type"] is FlowResultType.FORM
|
||||||
assert flow["step_id"] == "discovery_confirm"
|
assert flow["step_id"] == "discovery_confirm"
|
||||||
|
assert flow["description_placeholders"] == {"name": "test8266"}
|
||||||
|
|
||||||
mock_dashboard["configured"].append(
|
mock_dashboard["configured"].append(
|
||||||
{
|
{
|
||||||
@ -1397,6 +1490,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop(
|
|||||||
|
|
||||||
assert flow["type"] is FlowResultType.FORM
|
assert flow["type"] is FlowResultType.FORM
|
||||||
assert flow["step_id"] == "discovery_confirm"
|
assert flow["step_id"] == "discovery_confirm"
|
||||||
|
assert flow["description_placeholders"] == {"name": "test8266"}
|
||||||
|
|
||||||
mock_dashboard["configured"].append(
|
mock_dashboard["configured"].append(
|
||||||
{
|
{
|
||||||
@ -1462,6 +1556,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
|
|||||||
|
|
||||||
assert flow["type"] is FlowResultType.FORM
|
assert flow["type"] is FlowResultType.FORM
|
||||||
assert flow["step_id"] == "discovery_confirm"
|
assert flow["step_id"] == "discovery_confirm"
|
||||||
|
assert flow["description_placeholders"] == {"name": "test8266"}
|
||||||
|
|
||||||
await dashboard.async_get_dashboard(hass).async_refresh()
|
await dashboard.async_get_dashboard(hass).async_refresh()
|
||||||
|
|
||||||
@ -1473,6 +1568,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "test8266"}
|
||||||
|
|
||||||
|
|
||||||
async def test_option_flow_allow_service_calls(
|
async def test_option_flow_allow_service_calls(
|
||||||
@ -1585,6 +1681,7 @@ async def test_user_discovers_name_no_dashboard(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "test"}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
@ -1877,6 +1974,54 @@ async def test_reconfig_success_with_new_ip_same_name(
|
|||||||
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||||
|
async def test_reconfig_success_noise_psk_changes(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient
|
||||||
|
) -> None:
|
||||||
|
"""Test reconfig initiation with new ip and new noise psk."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: "127.0.0.1",
|
||||||
|
CONF_PORT: 6053,
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
CONF_DEVICE_NAME: "test",
|
||||||
|
CONF_NOISE_PSK: VALID_NOISE_PSK,
|
||||||
|
},
|
||||||
|
unique_id="11:22:33:44:55:aa",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await entry.start_reconfigure_flow(hass)
|
||||||
|
mock_client.device_info.side_effect = [
|
||||||
|
RequiresEncryptionAPIError,
|
||||||
|
InvalidEncryptionKeyAPIError("Wrong key", "test"),
|
||||||
|
DeviceInfo(uses_password=False, name="test", mac_address="11:22:33:44:55:aa"),
|
||||||
|
]
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "Mock Title (test)"}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "encryption_key"
|
||||||
|
assert result["description_placeholders"] == {"name": "Mock Title (test)"}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reconfigure_successful"
|
||||||
|
assert entry.data[CONF_HOST] == "127.0.0.1"
|
||||||
|
assert entry.data[CONF_DEVICE_NAME] == "test"
|
||||||
|
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||||
async def test_reconfig_name_conflict_with_existing_entry(
|
async def test_reconfig_name_conflict_with_existing_entry(
|
||||||
hass: HomeAssistant, mock_client: APIClient
|
hass: HomeAssistant, mock_client: APIClient
|
||||||
@ -1999,7 +2144,12 @@ async def test_reconfig_mac_used_by_other_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "test4",
|
||||||
|
"mac": "11:22:33:44:55:bb",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||||
|
@ -17,6 +17,7 @@ from aioesphomeapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
ATTR_RESTORED,
|
ATTR_RESTORED,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
@ -503,3 +504,40 @@ async def test_esphome_device_without_friendly_name(
|
|||||||
state = hass.states.get("binary_sensor.test_mybinary_sensor")
|
state = hass.states.get("binary_sensor.test_mybinary_sensor")
|
||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_without_name_device_with_friendly_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
mock_esphome_device: Callable[
|
||||||
|
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||||
|
Awaitable[MockESPHomeDevice],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test name and entity_id for a device a friendly name and an entity without a name."""
|
||||||
|
entity_info = [
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="mybinary_sensor",
|
||||||
|
key=1,
|
||||||
|
name="",
|
||||||
|
unique_id="my_binary_sensor",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
states = [
|
||||||
|
BinarySensorState(key=1, state=True, missing_state=False),
|
||||||
|
]
|
||||||
|
user_service = []
|
||||||
|
await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=entity_info,
|
||||||
|
user_service=user_service,
|
||||||
|
states=states,
|
||||||
|
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||||
|
)
|
||||||
|
state = hass.states.get("binary_sensor.mixer")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
# Make sure we have set the name to `None` as otherwise
|
||||||
|
# the friendly_name will be "The Best Mixer "
|
||||||
|
assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer"
|
||||||
|
@ -49,7 +49,12 @@ from homeassistant.setup import async_setup_component
|
|||||||
|
|
||||||
from .conftest import MockESPHomeDevice
|
from .conftest import MockESPHomeDevice
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_capture_events, async_mock_service
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_call_logger_set_level,
|
||||||
|
async_capture_events,
|
||||||
|
async_mock_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_esphome_device_subscribe_logs(
|
async def test_esphome_device_subscribe_logs(
|
||||||
@ -83,12 +88,9 @@ async def test_esphome_device_subscribe_logs(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.esphome", "DEBUG", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.esphome": "DEBUG"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert device.current_log_level == LogLevel.LOG_LEVEL_VERY_VERBOSE
|
assert device.current_log_level == LogLevel.LOG_LEVEL_VERY_VERBOSE
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
@ -118,26 +120,17 @@ async def test_esphome_device_subscribe_logs(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert "test_debug_log_message" in caplog.text
|
assert "test_debug_log_message" in caplog.text
|
||||||
|
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.esphome", "WARNING", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.esphome": "WARNING"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert device.current_log_level == LogLevel.LOG_LEVEL_WARN
|
assert device.current_log_level == LogLevel.LOG_LEVEL_WARN
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.esphome", "ERROR", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.esphome": "ERROR"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert device.current_log_level == LogLevel.LOG_LEVEL_ERROR
|
assert device.current_log_level == LogLevel.LOG_LEVEL_ERROR
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.esphome", "INFO", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.esphome": "INFO"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert device.current_log_level == LogLevel.LOG_LEVEL_CONFIG
|
assert device.current_log_level == LogLevel.LOG_LEVEL_CONFIG
|
||||||
|
|
||||||
|
|
||||||
@ -750,7 +743,12 @@ async def test_connection_aborted_wrong_device(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "test",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.184"
|
assert entry.data[CONF_HOST] == "192.168.43.184"
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(new_info.mock_calls) == 2
|
assert len(new_info.mock_calls) == 2
|
||||||
@ -819,7 +817,12 @@ async def test_connection_aborted_wrong_device_same_name(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured_updates"
|
||||||
|
assert result["description_placeholders"] == {
|
||||||
|
"title": "Mock Title",
|
||||||
|
"name": "test",
|
||||||
|
"mac": "11:22:33:44:55:aa",
|
||||||
|
}
|
||||||
assert entry.data[CONF_HOST] == "192.168.43.184"
|
assert entry.data[CONF_HOST] == "192.168.43.184"
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(new_info.mock_calls) == 2
|
assert len(new_info.mock_calls) == 2
|
||||||
@ -958,6 +961,7 @@ async def test_debug_logging(
|
|||||||
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||||
Awaitable[MockConfigEntry],
|
Awaitable[MockConfigEntry],
|
||||||
],
|
],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test enabling and disabling debug logging."""
|
"""Test enabling and disabling debug logging."""
|
||||||
assert await async_setup_component(hass, "logger", {"logger": {}})
|
assert await async_setup_component(hass, "logger", {"logger": {}})
|
||||||
@ -967,23 +971,15 @@ async def test_debug_logging(
|
|||||||
user_service=[],
|
user_service=[],
|
||||||
states=[],
|
states=[],
|
||||||
)
|
)
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"homeassistant.components.esphome", "DEBUG", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"homeassistant.components.esphome": "DEBUG"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
mock_client.set_debug.assert_has_calls([call(True)])
|
mock_client.set_debug.assert_has_calls([call(True)])
|
||||||
|
|
||||||
mock_client.reset_mock()
|
mock_client.reset_mock()
|
||||||
await hass.services.async_call(
|
|
||||||
"logger",
|
async with async_call_logger_set_level(
|
||||||
"set_level",
|
"homeassistant.components.esphome", "WARNING", hass=hass, caplog=caplog
|
||||||
{"homeassistant.components.esphome": "WARNING"},
|
):
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
mock_client.set_debug.assert_has_calls([call(False)])
|
mock_client.set_debug.assert_has_calls([call(False)])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
"""Test Govee light local."""
|
"""Test Govee light local."""
|
||||||
|
|
||||||
from errno import EADDRINUSE, ENETDOWN
|
from errno import EADDRINUSE, ENETDOWN
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||||
|
|
||||||
from govee_local_api import GoveeDevice
|
from govee_local_api import GoveeDevice
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.govee_light_local.const import DOMAIN
|
from homeassistant.components.govee_light_local.const import DOMAIN
|
||||||
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_BRIGHTNESS_PCT,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
|
ATTR_EFFECT,
|
||||||
|
ATTR_RGB_COLOR,
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES,
|
||||||
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
|
ColorMode,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .conftest import DEFAULT_CAPABILITIES, SCENE_CAPABILITIES
|
from .conftest import DEFAULT_CAPABILITIES, SCENE_CAPABILITIES
|
||||||
@ -197,8 +208,8 @@ async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> N
|
|||||||
assert light.state == "off"
|
assert light.state == "off"
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id},
|
{"entity_id": light.entity_id},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -211,8 +222,8 @@ async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> N
|
|||||||
|
|
||||||
# Turn off
|
# Turn off
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_off",
|
SERVICE_TURN_OFF,
|
||||||
{"entity_id": light.entity_id},
|
{"entity_id": light.entity_id},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -224,6 +235,77 @@ async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> N
|
|||||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], False)
|
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("attribute", "value", "mock_call", "mock_call_args", "mock_call_kwargs"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
ATTR_RGB_COLOR,
|
||||||
|
[100, 255, 50],
|
||||||
|
"set_color",
|
||||||
|
[],
|
||||||
|
{"temperature": None, "rgb": (100, 255, 50)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
|
4400,
|
||||||
|
"set_color",
|
||||||
|
[],
|
||||||
|
{"temperature": 4400, "rgb": None},
|
||||||
|
),
|
||||||
|
(ATTR_EFFECT, "sunrise", "set_scene", ["sunrise"], {}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_turn_on_call_order(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_govee_api: MagicMock,
|
||||||
|
attribute: str,
|
||||||
|
value: str | int | list[int],
|
||||||
|
mock_call: str,
|
||||||
|
mock_call_args: list[str],
|
||||||
|
mock_call_kwargs: dict[str, any],
|
||||||
|
) -> None:
|
||||||
|
"""Test that turn_on is called after set_brightness/set_color/set_preset."""
|
||||||
|
mock_govee_api.devices = [
|
||||||
|
GoveeDevice(
|
||||||
|
controller=mock_govee_api,
|
||||||
|
ip="192.168.1.100",
|
||||||
|
fingerprint="asdawdqwdqwd",
|
||||||
|
sku="H615A",
|
||||||
|
capabilities=SCENE_CAPABILITIES,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
light = hass.states.get("light.H615A")
|
||||||
|
assert light is not None
|
||||||
|
assert light.state == "off"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{"entity_id": light.entity_id, ATTR_BRIGHTNESS_PCT: 50, attribute: value},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_govee_api.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.set_brightness(mock_govee_api.devices[0], 50),
|
||||||
|
getattr(call, mock_call)(
|
||||||
|
mock_govee_api.devices[0], *mock_call_args, **mock_call_kwargs
|
||||||
|
),
|
||||||
|
call.turn_on_off(mock_govee_api.devices[0], True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
||||||
"""Test changing brightness."""
|
"""Test changing brightness."""
|
||||||
mock_govee_api.devices = [
|
mock_govee_api.devices = [
|
||||||
@ -249,8 +331,8 @@ async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock)
|
|||||||
assert light.state == "off"
|
assert light.state == "off"
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "brightness_pct": 50},
|
{"entity_id": light.entity_id, "brightness_pct": 50},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -260,12 +342,12 @@ async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock)
|
|||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 50)
|
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 50)
|
||||||
assert light.attributes["brightness"] == 127
|
assert light.attributes[ATTR_BRIGHTNESS] == 127
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "brightness": 255},
|
{"entity_id": light.entity_id, ATTR_BRIGHTNESS: 255},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -273,13 +355,13 @@ async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock)
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["brightness"] == 255
|
assert light.attributes[ATTR_BRIGHTNESS] == 255
|
||||||
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 100)
|
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 100)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "brightness": 255},
|
{"entity_id": light.entity_id, ATTR_BRIGHTNESS: 255},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -287,7 +369,7 @@ async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock)
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["brightness"] == 255
|
assert light.attributes[ATTR_BRIGHTNESS] == 255
|
||||||
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 100)
|
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 100)
|
||||||
|
|
||||||
|
|
||||||
@ -316,9 +398,9 @@ async def test_light_color(hass: HomeAssistant, mock_govee_api: MagicMock) -> No
|
|||||||
assert light.state == "off"
|
assert light.state == "off"
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "rgb_color": [100, 255, 50]},
|
{"entity_id": light.entity_id, ATTR_RGB_COLOR: [100, 255, 50]},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -326,7 +408,7 @@ async def test_light_color(hass: HomeAssistant, mock_govee_api: MagicMock) -> No
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["rgb_color"] == (100, 255, 50)
|
assert light.attributes[ATTR_RGB_COLOR] == (100, 255, 50)
|
||||||
assert light.attributes["color_mode"] == ColorMode.RGB
|
assert light.attributes["color_mode"] == ColorMode.RGB
|
||||||
|
|
||||||
mock_govee_api.set_color.assert_awaited_with(
|
mock_govee_api.set_color.assert_awaited_with(
|
||||||
@ -334,8 +416,8 @@ async def test_light_color(hass: HomeAssistant, mock_govee_api: MagicMock) -> No
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "kelvin": 4400},
|
{"entity_id": light.entity_id, "kelvin": 4400},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -378,9 +460,9 @@ async def test_scene_on(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
|||||||
assert light.state == "off"
|
assert light.state == "off"
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "sunrise"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "sunrise"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -388,7 +470,7 @@ async def test_scene_on(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] == "sunrise"
|
assert light.attributes[ATTR_EFFECT] == "sunrise"
|
||||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
||||||
|
|
||||||
|
|
||||||
@ -422,16 +504,16 @@ async def test_scene_restore_rgb(
|
|||||||
|
|
||||||
# Set initial color
|
# Set initial color
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "rgb_color": initial_color},
|
{"entity_id": light.entity_id, ATTR_RGB_COLOR: initial_color},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "brightness": 255},
|
{"entity_id": light.entity_id, ATTR_BRIGHTNESS: 255},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -439,15 +521,15 @@ async def test_scene_restore_rgb(
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["rgb_color"] == initial_color
|
assert light.attributes[ATTR_RGB_COLOR] == initial_color
|
||||||
assert light.attributes["brightness"] == 255
|
assert light.attributes[ATTR_BRIGHTNESS] == 255
|
||||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
||||||
|
|
||||||
# Activate scene
|
# Activate scene
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "sunrise"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "sunrise"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -455,14 +537,14 @@ async def test_scene_restore_rgb(
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] == "sunrise"
|
assert light.attributes[ATTR_EFFECT] == "sunrise"
|
||||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
||||||
|
|
||||||
# Deactivate scene
|
# Deactivate scene
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "none"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "none"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -470,9 +552,9 @@ async def test_scene_restore_rgb(
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] is None
|
assert light.attributes[ATTR_EFFECT] is None
|
||||||
assert light.attributes["rgb_color"] == initial_color
|
assert light.attributes[ATTR_RGB_COLOR] == initial_color
|
||||||
assert light.attributes["brightness"] == 255
|
assert light.attributes[ATTR_BRIGHTNESS] == 255
|
||||||
|
|
||||||
|
|
||||||
async def test_scene_restore_temperature(
|
async def test_scene_restore_temperature(
|
||||||
@ -505,8 +587,8 @@ async def test_scene_restore_temperature(
|
|||||||
|
|
||||||
# Set initial color
|
# Set initial color
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "color_temp_kelvin": initial_color},
|
{"entity_id": light.entity_id, "color_temp_kelvin": initial_color},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -520,9 +602,9 @@ async def test_scene_restore_temperature(
|
|||||||
|
|
||||||
# Activate scene
|
# Activate scene
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "sunrise"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "sunrise"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -530,14 +612,14 @@ async def test_scene_restore_temperature(
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] == "sunrise"
|
assert light.attributes[ATTR_EFFECT] == "sunrise"
|
||||||
mock_govee_api.set_scene.assert_awaited_with(mock_govee_api.devices[0], "sunrise")
|
mock_govee_api.set_scene.assert_awaited_with(mock_govee_api.devices[0], "sunrise")
|
||||||
|
|
||||||
# Deactivate scene
|
# Deactivate scene
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "none"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "none"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -545,7 +627,7 @@ async def test_scene_restore_temperature(
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] is None
|
assert light.attributes[ATTR_EFFECT] is None
|
||||||
assert light.attributes["color_temp_kelvin"] == initial_color
|
assert light.attributes["color_temp_kelvin"] == initial_color
|
||||||
|
|
||||||
|
|
||||||
@ -577,16 +659,16 @@ async def test_scene_none(hass: HomeAssistant, mock_govee_api: MagicMock) -> Non
|
|||||||
|
|
||||||
# Set initial color
|
# Set initial color
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "rgb_color": initial_color},
|
{"entity_id": light.entity_id, ATTR_RGB_COLOR: initial_color},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "brightness": 255},
|
{"entity_id": light.entity_id, ATTR_BRIGHTNESS: 255},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -594,21 +676,20 @@ async def test_scene_none(hass: HomeAssistant, mock_govee_api: MagicMock) -> Non
|
|||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["rgb_color"] == initial_color
|
assert light.attributes[ATTR_RGB_COLOR] == initial_color
|
||||||
assert light.attributes["brightness"] == 255
|
assert light.attributes[ATTR_BRIGHTNESS] == 255
|
||||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
||||||
|
|
||||||
# Activate scene
|
# Activate scene
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": light.entity_id, "effect": "none"},
|
{"entity_id": light.entity_id, ATTR_EFFECT: "none"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
light = hass.states.get("light.H615A")
|
light = hass.states.get("light.H615A")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.state == "on"
|
assert light.state == "on"
|
||||||
assert light.attributes["effect"] is None
|
assert light.attributes[ATTR_EFFECT] is None
|
||||||
mock_govee_api.set_scene.assert_not_called()
|
mock_govee_api.set_scene.assert_not_called()
|
||||||
|
@ -969,6 +969,135 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul
|
|||||||
assert hass.states.get("sensor.sensor4").state == "87.5"
|
assert hass.states.get("sensor.sensor4").state == "87.5"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_start_from_history_then_watch_state_changes_sliding(
|
||||||
|
recorder_mock: Recorder,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test we startup from history and switch to watching state changes.
|
||||||
|
|
||||||
|
With a sliding window, history_stats does not requery the recorder.
|
||||||
|
"""
|
||||||
|
await hass.config.async_set_time_zone("UTC")
|
||||||
|
utcnow = dt_util.utcnow()
|
||||||
|
start_time = utcnow.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
time = start_time
|
||||||
|
|
||||||
|
def _fake_states(*args, **kwargs):
|
||||||
|
return {
|
||||||
|
"binary_sensor.state": [
|
||||||
|
ha.State(
|
||||||
|
"binary_sensor.state",
|
||||||
|
"off",
|
||||||
|
last_changed=start_time - timedelta(hours=1),
|
||||||
|
last_updated=start_time - timedelta(hours=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.recorder.history.state_changes_during_period",
|
||||||
|
_fake_states,
|
||||||
|
),
|
||||||
|
freeze_time(start_time),
|
||||||
|
):
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": [
|
||||||
|
{
|
||||||
|
"platform": "history_stats",
|
||||||
|
"entity_id": "binary_sensor.state",
|
||||||
|
"name": f"sensor{i}",
|
||||||
|
"state": "on",
|
||||||
|
"end": "{{ utcnow() }}",
|
||||||
|
"duration": {"hours": 1},
|
||||||
|
"type": sensor_type,
|
||||||
|
}
|
||||||
|
for i, sensor_type in enumerate(["time", "ratio", "count"])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
await async_update_entity(hass, f"sensor.sensor{i}")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "0"
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
hass.states.async_set("binary_sensor.state", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "1"
|
||||||
|
|
||||||
|
# After sensor has been on for 15 minutes, check state
|
||||||
|
time += timedelta(minutes=15) # 00:15
|
||||||
|
with freeze_time(time):
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.25"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "25.0"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "1"
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
hass.states.async_set("binary_sensor.state", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
time += timedelta(minutes=30) # 00:45
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.25"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "25.0"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "1"
|
||||||
|
|
||||||
|
time += timedelta(minutes=20) # 01:05
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Sliding window will have started to erase the initial on period, so now it will only be on for 10 minutes
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.17"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "16.7"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "1"
|
||||||
|
|
||||||
|
time += timedelta(minutes=5) # 01:10
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Sliding window will continue to erase the initial on period, so now it will only be on for 5 minutes
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.08"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "8.3"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "1"
|
||||||
|
|
||||||
|
time += timedelta(minutes=10) # 01:20
|
||||||
|
|
||||||
|
with freeze_time(time):
|
||||||
|
async_fire_time_changed(hass, time)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.sensor0").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor1").state == "0.0"
|
||||||
|
assert hass.states.get("sensor.sensor2").state == "0"
|
||||||
|
|
||||||
|
|
||||||
async def test_does_not_work_into_the_future(
|
async def test_does_not_work_into_the_future(
|
||||||
recorder_mock: Recorder, hass: HomeAssistant
|
recorder_mock: Recorder, hass: HomeAssistant
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -1366,10 +1495,6 @@ async def test_measure_from_end_going_backwards(
|
|||||||
|
|
||||||
past_next_update = start_time + timedelta(minutes=30)
|
past_next_update = start_time + timedelta(minutes=30)
|
||||||
with (
|
with (
|
||||||
patch(
|
|
||||||
"homeassistant.components.recorder.history.state_changes_during_period",
|
|
||||||
_fake_states,
|
|
||||||
),
|
|
||||||
freeze_time(past_next_update),
|
freeze_time(past_next_update),
|
||||||
):
|
):
|
||||||
async_fire_time_changed(hass, past_next_update)
|
async_fire_time_changed(hass, past_next_update)
|
||||||
@ -1526,29 +1651,10 @@ async def test_state_change_during_window_rollover(
|
|||||||
|
|
||||||
assert hass.states.get("sensor.sensor1").state == "11.98"
|
assert hass.states.get("sensor.sensor1").state == "11.98"
|
||||||
|
|
||||||
# One minute has passed and the time has now rolled over into a new day, resetting the recorder window. The sensor will then query the database for updates,
|
# One minute has passed and the time has now rolled over into a new day, resetting the recorder window.
|
||||||
# and will see that the sensor is ON starting from midnight.
|
# The sensor will be ON since midnight.
|
||||||
t3 = t2 + timedelta(minutes=1)
|
t3 = t2 + timedelta(minutes=1)
|
||||||
|
with freeze_time(t3):
|
||||||
def _fake_states_t3(*args, **kwargs):
|
|
||||||
return {
|
|
||||||
"binary_sensor.state": [
|
|
||||||
ha.State(
|
|
||||||
"binary_sensor.state",
|
|
||||||
"on",
|
|
||||||
last_changed=t3.replace(hour=0, minute=0, second=0, microsecond=0),
|
|
||||||
last_updated=t3.replace(hour=0, minute=0, second=0, microsecond=0),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.recorder.history.state_changes_during_period",
|
|
||||||
_fake_states_t3,
|
|
||||||
),
|
|
||||||
freeze_time(t3),
|
|
||||||
):
|
|
||||||
# The sensor turns off around this time, before the sensor does its normal polled update.
|
# The sensor turns off around this time, before the sensor does its normal polled update.
|
||||||
hass.states.async_set("binary_sensor.state", "off")
|
hass.states.async_set("binary_sensor.state", "off")
|
||||||
await hass.async_block_till_done(wait_background_tasks=True)
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
@ -22,7 +22,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.ssl import server_context_intermediate, server_context_modern
|
from homeassistant.util.ssl import server_context_intermediate, server_context_modern
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_call_logger_set_level, async_fire_time_changed
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
@ -505,24 +505,18 @@ async def test_logging(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
hass.states.async_set("logging.entity", "hello")
|
hass.states.async_set("logging.entity", "hello")
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"aiohttp.access", "INFO", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"aiohttp.access": "info"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get("/api/states/logging.entity")
|
response = await client.get("/api/states/logging.entity")
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
assert "GET /api/states/logging.entity" in caplog.text
|
assert "GET /api/states/logging.entity" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"aiohttp.access", "WARNING", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"aiohttp.access": "warning"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
response = await client.get("/api/states/logging.entity")
|
response = await client.get("/api/states/logging.entity")
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
assert "GET /api/states/logging.entity" not in caplog.text
|
assert "GET /api/states/logging.entity" not in caplog.text
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
'device_id': <ANY>,
|
'device_id': <ANY>,
|
||||||
'disabled_by': None,
|
'disabled_by': None,
|
||||||
'domain': 'sensor',
|
'domain': 'sensor',
|
||||||
'entity_category': None,
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
'entity_id': 'sensor.test_mower_1_cutting_blade_usage_time',
|
'entity_id': 'sensor.test_mower_1_cutting_blade_usage_time',
|
||||||
'has_entity_name': True,
|
'has_entity_name': True,
|
||||||
'hidden_by': None,
|
'hidden_by': None,
|
||||||
@ -120,7 +120,7 @@
|
|||||||
'device_id': <ANY>,
|
'device_id': <ANY>,
|
||||||
'disabled_by': None,
|
'disabled_by': None,
|
||||||
'domain': 'sensor',
|
'domain': 'sensor',
|
||||||
'entity_category': None,
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
'entity_id': 'sensor.test_mower_1_downtime',
|
'entity_id': 'sensor.test_mower_1_downtime',
|
||||||
'has_entity_name': True,
|
'has_entity_name': True,
|
||||||
'hidden_by': None,
|
'hidden_by': None,
|
||||||
@ -171,7 +171,6 @@
|
|||||||
'area_id': None,
|
'area_id': None,
|
||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
'options': list([
|
'options': list([
|
||||||
'no_error',
|
|
||||||
'alarm_mower_in_motion',
|
'alarm_mower_in_motion',
|
||||||
'alarm_mower_lifted',
|
'alarm_mower_lifted',
|
||||||
'alarm_mower_stopped',
|
'alarm_mower_stopped',
|
||||||
@ -180,13 +179,11 @@
|
|||||||
'alarm_outside_geofence',
|
'alarm_outside_geofence',
|
||||||
'angular_sensor_problem',
|
'angular_sensor_problem',
|
||||||
'battery_problem',
|
'battery_problem',
|
||||||
'battery_problem',
|
|
||||||
'battery_restriction_due_to_ambient_temperature',
|
'battery_restriction_due_to_ambient_temperature',
|
||||||
'can_error',
|
'can_error',
|
||||||
'charging_current_too_high',
|
'charging_current_too_high',
|
||||||
'charging_station_blocked',
|
'charging_station_blocked',
|
||||||
'charging_system_problem',
|
'charging_system_problem',
|
||||||
'charging_system_problem',
|
|
||||||
'collision_sensor_defect',
|
'collision_sensor_defect',
|
||||||
'collision_sensor_error',
|
'collision_sensor_error',
|
||||||
'collision_sensor_problem_front',
|
'collision_sensor_problem_front',
|
||||||
@ -197,24 +194,18 @@
|
|||||||
'connection_changed',
|
'connection_changed',
|
||||||
'connection_not_changed',
|
'connection_not_changed',
|
||||||
'connectivity_problem',
|
'connectivity_problem',
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_settings_restored',
|
'connectivity_settings_restored',
|
||||||
'cutting_drive_motor_1_defect',
|
'cutting_drive_motor_1_defect',
|
||||||
'cutting_drive_motor_2_defect',
|
'cutting_drive_motor_2_defect',
|
||||||
'cutting_drive_motor_3_defect',
|
'cutting_drive_motor_3_defect',
|
||||||
'cutting_height_blocked',
|
'cutting_height_blocked',
|
||||||
'cutting_height_problem',
|
|
||||||
'cutting_height_problem_curr',
|
'cutting_height_problem_curr',
|
||||||
'cutting_height_problem_dir',
|
'cutting_height_problem_dir',
|
||||||
'cutting_height_problem_drive',
|
'cutting_height_problem_drive',
|
||||||
|
'cutting_height_problem',
|
||||||
'cutting_motor_problem',
|
'cutting_motor_problem',
|
||||||
'cutting_stopped_slope_too_steep',
|
'cutting_stopped_slope_too_steep',
|
||||||
'cutting_system_blocked',
|
'cutting_system_blocked',
|
||||||
'cutting_system_blocked',
|
|
||||||
'cutting_system_imbalance_warning',
|
'cutting_system_imbalance_warning',
|
||||||
'cutting_system_major_imbalance',
|
'cutting_system_major_imbalance',
|
||||||
'destination_not_reachable',
|
'destination_not_reachable',
|
||||||
@ -222,13 +213,9 @@
|
|||||||
'docking_sensor_defect',
|
'docking_sensor_defect',
|
||||||
'electronic_problem',
|
'electronic_problem',
|
||||||
'empty_battery',
|
'empty_battery',
|
||||||
'error',
|
|
||||||
'error_at_power_up',
|
|
||||||
'fatal_error',
|
|
||||||
'folding_cutting_deck_sensor_defect',
|
'folding_cutting_deck_sensor_defect',
|
||||||
'folding_sensor_activated',
|
'folding_sensor_activated',
|
||||||
'geofence_problem',
|
'geofence_problem',
|
||||||
'geofence_problem',
|
|
||||||
'gps_navigation_problem',
|
'gps_navigation_problem',
|
||||||
'guide_1_not_found',
|
'guide_1_not_found',
|
||||||
'guide_2_not_found',
|
'guide_2_not_found',
|
||||||
@ -246,7 +233,6 @@
|
|||||||
'lift_sensor_defect',
|
'lift_sensor_defect',
|
||||||
'lifted',
|
'lifted',
|
||||||
'limited_cutting_height_range',
|
'limited_cutting_height_range',
|
||||||
'limited_cutting_height_range',
|
|
||||||
'loop_sensor_defect',
|
'loop_sensor_defect',
|
||||||
'loop_sensor_problem_front',
|
'loop_sensor_problem_front',
|
||||||
'loop_sensor_problem_left',
|
'loop_sensor_problem_left',
|
||||||
@ -259,6 +245,7 @@
|
|||||||
'no_accurate_position_from_satellites',
|
'no_accurate_position_from_satellites',
|
||||||
'no_confirmed_position',
|
'no_confirmed_position',
|
||||||
'no_drive',
|
'no_drive',
|
||||||
|
'no_error',
|
||||||
'no_loop_signal',
|
'no_loop_signal',
|
||||||
'no_power_in_charging_station',
|
'no_power_in_charging_station',
|
||||||
'no_response_from_charger',
|
'no_response_from_charger',
|
||||||
@ -269,9 +256,6 @@
|
|||||||
'safety_function_faulty',
|
'safety_function_faulty',
|
||||||
'settings_restored',
|
'settings_restored',
|
||||||
'sim_card_locked',
|
'sim_card_locked',
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_not_found',
|
'sim_card_not_found',
|
||||||
'sim_card_requires_pin',
|
'sim_card_requires_pin',
|
||||||
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
||||||
@ -281,13 +265,6 @@
|
|||||||
'stuck_in_charging_station',
|
'stuck_in_charging_station',
|
||||||
'switch_cord_problem',
|
'switch_cord_problem',
|
||||||
'temporary_battery_problem',
|
'temporary_battery_problem',
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'tilt_sensor_problem',
|
'tilt_sensor_problem',
|
||||||
'too_high_discharge_current',
|
'too_high_discharge_current',
|
||||||
'too_high_internal_current',
|
'too_high_internal_current',
|
||||||
@ -317,6 +294,13 @@
|
|||||||
'wrong_loop_signal',
|
'wrong_loop_signal',
|
||||||
'wrong_pin_code',
|
'wrong_pin_code',
|
||||||
'zone_generator_problem',
|
'zone_generator_problem',
|
||||||
|
'error_at_power_up',
|
||||||
|
'error',
|
||||||
|
'fatal_error',
|
||||||
|
'off',
|
||||||
|
'stopped',
|
||||||
|
'wait_power_up',
|
||||||
|
'wait_updating',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'config_entry_id': <ANY>,
|
'config_entry_id': <ANY>,
|
||||||
@ -353,7 +337,6 @@
|
|||||||
'device_class': 'enum',
|
'device_class': 'enum',
|
||||||
'friendly_name': 'Test Mower 1 Error',
|
'friendly_name': 'Test Mower 1 Error',
|
||||||
'options': list([
|
'options': list([
|
||||||
'no_error',
|
|
||||||
'alarm_mower_in_motion',
|
'alarm_mower_in_motion',
|
||||||
'alarm_mower_lifted',
|
'alarm_mower_lifted',
|
||||||
'alarm_mower_stopped',
|
'alarm_mower_stopped',
|
||||||
@ -362,13 +345,11 @@
|
|||||||
'alarm_outside_geofence',
|
'alarm_outside_geofence',
|
||||||
'angular_sensor_problem',
|
'angular_sensor_problem',
|
||||||
'battery_problem',
|
'battery_problem',
|
||||||
'battery_problem',
|
|
||||||
'battery_restriction_due_to_ambient_temperature',
|
'battery_restriction_due_to_ambient_temperature',
|
||||||
'can_error',
|
'can_error',
|
||||||
'charging_current_too_high',
|
'charging_current_too_high',
|
||||||
'charging_station_blocked',
|
'charging_station_blocked',
|
||||||
'charging_system_problem',
|
'charging_system_problem',
|
||||||
'charging_system_problem',
|
|
||||||
'collision_sensor_defect',
|
'collision_sensor_defect',
|
||||||
'collision_sensor_error',
|
'collision_sensor_error',
|
||||||
'collision_sensor_problem_front',
|
'collision_sensor_problem_front',
|
||||||
@ -379,24 +360,18 @@
|
|||||||
'connection_changed',
|
'connection_changed',
|
||||||
'connection_not_changed',
|
'connection_not_changed',
|
||||||
'connectivity_problem',
|
'connectivity_problem',
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_settings_restored',
|
'connectivity_settings_restored',
|
||||||
'cutting_drive_motor_1_defect',
|
'cutting_drive_motor_1_defect',
|
||||||
'cutting_drive_motor_2_defect',
|
'cutting_drive_motor_2_defect',
|
||||||
'cutting_drive_motor_3_defect',
|
'cutting_drive_motor_3_defect',
|
||||||
'cutting_height_blocked',
|
'cutting_height_blocked',
|
||||||
'cutting_height_problem',
|
|
||||||
'cutting_height_problem_curr',
|
'cutting_height_problem_curr',
|
||||||
'cutting_height_problem_dir',
|
'cutting_height_problem_dir',
|
||||||
'cutting_height_problem_drive',
|
'cutting_height_problem_drive',
|
||||||
|
'cutting_height_problem',
|
||||||
'cutting_motor_problem',
|
'cutting_motor_problem',
|
||||||
'cutting_stopped_slope_too_steep',
|
'cutting_stopped_slope_too_steep',
|
||||||
'cutting_system_blocked',
|
'cutting_system_blocked',
|
||||||
'cutting_system_blocked',
|
|
||||||
'cutting_system_imbalance_warning',
|
'cutting_system_imbalance_warning',
|
||||||
'cutting_system_major_imbalance',
|
'cutting_system_major_imbalance',
|
||||||
'destination_not_reachable',
|
'destination_not_reachable',
|
||||||
@ -404,13 +379,9 @@
|
|||||||
'docking_sensor_defect',
|
'docking_sensor_defect',
|
||||||
'electronic_problem',
|
'electronic_problem',
|
||||||
'empty_battery',
|
'empty_battery',
|
||||||
'error',
|
|
||||||
'error_at_power_up',
|
|
||||||
'fatal_error',
|
|
||||||
'folding_cutting_deck_sensor_defect',
|
'folding_cutting_deck_sensor_defect',
|
||||||
'folding_sensor_activated',
|
'folding_sensor_activated',
|
||||||
'geofence_problem',
|
'geofence_problem',
|
||||||
'geofence_problem',
|
|
||||||
'gps_navigation_problem',
|
'gps_navigation_problem',
|
||||||
'guide_1_not_found',
|
'guide_1_not_found',
|
||||||
'guide_2_not_found',
|
'guide_2_not_found',
|
||||||
@ -428,7 +399,6 @@
|
|||||||
'lift_sensor_defect',
|
'lift_sensor_defect',
|
||||||
'lifted',
|
'lifted',
|
||||||
'limited_cutting_height_range',
|
'limited_cutting_height_range',
|
||||||
'limited_cutting_height_range',
|
|
||||||
'loop_sensor_defect',
|
'loop_sensor_defect',
|
||||||
'loop_sensor_problem_front',
|
'loop_sensor_problem_front',
|
||||||
'loop_sensor_problem_left',
|
'loop_sensor_problem_left',
|
||||||
@ -441,6 +411,7 @@
|
|||||||
'no_accurate_position_from_satellites',
|
'no_accurate_position_from_satellites',
|
||||||
'no_confirmed_position',
|
'no_confirmed_position',
|
||||||
'no_drive',
|
'no_drive',
|
||||||
|
'no_error',
|
||||||
'no_loop_signal',
|
'no_loop_signal',
|
||||||
'no_power_in_charging_station',
|
'no_power_in_charging_station',
|
||||||
'no_response_from_charger',
|
'no_response_from_charger',
|
||||||
@ -451,9 +422,6 @@
|
|||||||
'safety_function_faulty',
|
'safety_function_faulty',
|
||||||
'settings_restored',
|
'settings_restored',
|
||||||
'sim_card_locked',
|
'sim_card_locked',
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_not_found',
|
'sim_card_not_found',
|
||||||
'sim_card_requires_pin',
|
'sim_card_requires_pin',
|
||||||
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
||||||
@ -463,13 +431,6 @@
|
|||||||
'stuck_in_charging_station',
|
'stuck_in_charging_station',
|
||||||
'switch_cord_problem',
|
'switch_cord_problem',
|
||||||
'temporary_battery_problem',
|
'temporary_battery_problem',
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'tilt_sensor_problem',
|
'tilt_sensor_problem',
|
||||||
'too_high_discharge_current',
|
'too_high_discharge_current',
|
||||||
'too_high_internal_current',
|
'too_high_internal_current',
|
||||||
@ -499,6 +460,13 @@
|
|||||||
'wrong_loop_signal',
|
'wrong_loop_signal',
|
||||||
'wrong_pin_code',
|
'wrong_pin_code',
|
||||||
'zone_generator_problem',
|
'zone_generator_problem',
|
||||||
|
'error_at_power_up',
|
||||||
|
'error',
|
||||||
|
'fatal_error',
|
||||||
|
'off',
|
||||||
|
'stopped',
|
||||||
|
'wait_power_up',
|
||||||
|
'wait_updating',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
@ -1280,7 +1248,7 @@
|
|||||||
'device_id': <ANY>,
|
'device_id': <ANY>,
|
||||||
'disabled_by': None,
|
'disabled_by': None,
|
||||||
'domain': 'sensor',
|
'domain': 'sensor',
|
||||||
'entity_category': None,
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
'entity_id': 'sensor.test_mower_1_uptime',
|
'entity_id': 'sensor.test_mower_1_uptime',
|
||||||
'has_entity_name': True,
|
'has_entity_name': True,
|
||||||
'hidden_by': None,
|
'hidden_by': None,
|
||||||
@ -1449,7 +1417,6 @@
|
|||||||
'area_id': None,
|
'area_id': None,
|
||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
'options': list([
|
'options': list([
|
||||||
'no_error',
|
|
||||||
'alarm_mower_in_motion',
|
'alarm_mower_in_motion',
|
||||||
'alarm_mower_lifted',
|
'alarm_mower_lifted',
|
||||||
'alarm_mower_stopped',
|
'alarm_mower_stopped',
|
||||||
@ -1458,13 +1425,11 @@
|
|||||||
'alarm_outside_geofence',
|
'alarm_outside_geofence',
|
||||||
'angular_sensor_problem',
|
'angular_sensor_problem',
|
||||||
'battery_problem',
|
'battery_problem',
|
||||||
'battery_problem',
|
|
||||||
'battery_restriction_due_to_ambient_temperature',
|
'battery_restriction_due_to_ambient_temperature',
|
||||||
'can_error',
|
'can_error',
|
||||||
'charging_current_too_high',
|
'charging_current_too_high',
|
||||||
'charging_station_blocked',
|
'charging_station_blocked',
|
||||||
'charging_system_problem',
|
'charging_system_problem',
|
||||||
'charging_system_problem',
|
|
||||||
'collision_sensor_defect',
|
'collision_sensor_defect',
|
||||||
'collision_sensor_error',
|
'collision_sensor_error',
|
||||||
'collision_sensor_problem_front',
|
'collision_sensor_problem_front',
|
||||||
@ -1475,24 +1440,18 @@
|
|||||||
'connection_changed',
|
'connection_changed',
|
||||||
'connection_not_changed',
|
'connection_not_changed',
|
||||||
'connectivity_problem',
|
'connectivity_problem',
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_settings_restored',
|
'connectivity_settings_restored',
|
||||||
'cutting_drive_motor_1_defect',
|
'cutting_drive_motor_1_defect',
|
||||||
'cutting_drive_motor_2_defect',
|
'cutting_drive_motor_2_defect',
|
||||||
'cutting_drive_motor_3_defect',
|
'cutting_drive_motor_3_defect',
|
||||||
'cutting_height_blocked',
|
'cutting_height_blocked',
|
||||||
'cutting_height_problem',
|
|
||||||
'cutting_height_problem_curr',
|
'cutting_height_problem_curr',
|
||||||
'cutting_height_problem_dir',
|
'cutting_height_problem_dir',
|
||||||
'cutting_height_problem_drive',
|
'cutting_height_problem_drive',
|
||||||
|
'cutting_height_problem',
|
||||||
'cutting_motor_problem',
|
'cutting_motor_problem',
|
||||||
'cutting_stopped_slope_too_steep',
|
'cutting_stopped_slope_too_steep',
|
||||||
'cutting_system_blocked',
|
'cutting_system_blocked',
|
||||||
'cutting_system_blocked',
|
|
||||||
'cutting_system_imbalance_warning',
|
'cutting_system_imbalance_warning',
|
||||||
'cutting_system_major_imbalance',
|
'cutting_system_major_imbalance',
|
||||||
'destination_not_reachable',
|
'destination_not_reachable',
|
||||||
@ -1500,13 +1459,9 @@
|
|||||||
'docking_sensor_defect',
|
'docking_sensor_defect',
|
||||||
'electronic_problem',
|
'electronic_problem',
|
||||||
'empty_battery',
|
'empty_battery',
|
||||||
'error',
|
|
||||||
'error_at_power_up',
|
|
||||||
'fatal_error',
|
|
||||||
'folding_cutting_deck_sensor_defect',
|
'folding_cutting_deck_sensor_defect',
|
||||||
'folding_sensor_activated',
|
'folding_sensor_activated',
|
||||||
'geofence_problem',
|
'geofence_problem',
|
||||||
'geofence_problem',
|
|
||||||
'gps_navigation_problem',
|
'gps_navigation_problem',
|
||||||
'guide_1_not_found',
|
'guide_1_not_found',
|
||||||
'guide_2_not_found',
|
'guide_2_not_found',
|
||||||
@ -1524,7 +1479,6 @@
|
|||||||
'lift_sensor_defect',
|
'lift_sensor_defect',
|
||||||
'lifted',
|
'lifted',
|
||||||
'limited_cutting_height_range',
|
'limited_cutting_height_range',
|
||||||
'limited_cutting_height_range',
|
|
||||||
'loop_sensor_defect',
|
'loop_sensor_defect',
|
||||||
'loop_sensor_problem_front',
|
'loop_sensor_problem_front',
|
||||||
'loop_sensor_problem_left',
|
'loop_sensor_problem_left',
|
||||||
@ -1537,6 +1491,7 @@
|
|||||||
'no_accurate_position_from_satellites',
|
'no_accurate_position_from_satellites',
|
||||||
'no_confirmed_position',
|
'no_confirmed_position',
|
||||||
'no_drive',
|
'no_drive',
|
||||||
|
'no_error',
|
||||||
'no_loop_signal',
|
'no_loop_signal',
|
||||||
'no_power_in_charging_station',
|
'no_power_in_charging_station',
|
||||||
'no_response_from_charger',
|
'no_response_from_charger',
|
||||||
@ -1547,9 +1502,6 @@
|
|||||||
'safety_function_faulty',
|
'safety_function_faulty',
|
||||||
'settings_restored',
|
'settings_restored',
|
||||||
'sim_card_locked',
|
'sim_card_locked',
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_not_found',
|
'sim_card_not_found',
|
||||||
'sim_card_requires_pin',
|
'sim_card_requires_pin',
|
||||||
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
||||||
@ -1559,13 +1511,6 @@
|
|||||||
'stuck_in_charging_station',
|
'stuck_in_charging_station',
|
||||||
'switch_cord_problem',
|
'switch_cord_problem',
|
||||||
'temporary_battery_problem',
|
'temporary_battery_problem',
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'tilt_sensor_problem',
|
'tilt_sensor_problem',
|
||||||
'too_high_discharge_current',
|
'too_high_discharge_current',
|
||||||
'too_high_internal_current',
|
'too_high_internal_current',
|
||||||
@ -1595,6 +1540,13 @@
|
|||||||
'wrong_loop_signal',
|
'wrong_loop_signal',
|
||||||
'wrong_pin_code',
|
'wrong_pin_code',
|
||||||
'zone_generator_problem',
|
'zone_generator_problem',
|
||||||
|
'error_at_power_up',
|
||||||
|
'error',
|
||||||
|
'fatal_error',
|
||||||
|
'off',
|
||||||
|
'stopped',
|
||||||
|
'wait_power_up',
|
||||||
|
'wait_updating',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'config_entry_id': <ANY>,
|
'config_entry_id': <ANY>,
|
||||||
@ -1631,7 +1583,6 @@
|
|||||||
'device_class': 'enum',
|
'device_class': 'enum',
|
||||||
'friendly_name': 'Test Mower 2 Error',
|
'friendly_name': 'Test Mower 2 Error',
|
||||||
'options': list([
|
'options': list([
|
||||||
'no_error',
|
|
||||||
'alarm_mower_in_motion',
|
'alarm_mower_in_motion',
|
||||||
'alarm_mower_lifted',
|
'alarm_mower_lifted',
|
||||||
'alarm_mower_stopped',
|
'alarm_mower_stopped',
|
||||||
@ -1640,13 +1591,11 @@
|
|||||||
'alarm_outside_geofence',
|
'alarm_outside_geofence',
|
||||||
'angular_sensor_problem',
|
'angular_sensor_problem',
|
||||||
'battery_problem',
|
'battery_problem',
|
||||||
'battery_problem',
|
|
||||||
'battery_restriction_due_to_ambient_temperature',
|
'battery_restriction_due_to_ambient_temperature',
|
||||||
'can_error',
|
'can_error',
|
||||||
'charging_current_too_high',
|
'charging_current_too_high',
|
||||||
'charging_station_blocked',
|
'charging_station_blocked',
|
||||||
'charging_system_problem',
|
'charging_system_problem',
|
||||||
'charging_system_problem',
|
|
||||||
'collision_sensor_defect',
|
'collision_sensor_defect',
|
||||||
'collision_sensor_error',
|
'collision_sensor_error',
|
||||||
'collision_sensor_problem_front',
|
'collision_sensor_problem_front',
|
||||||
@ -1657,24 +1606,18 @@
|
|||||||
'connection_changed',
|
'connection_changed',
|
||||||
'connection_not_changed',
|
'connection_not_changed',
|
||||||
'connectivity_problem',
|
'connectivity_problem',
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_problem',
|
|
||||||
'connectivity_settings_restored',
|
'connectivity_settings_restored',
|
||||||
'cutting_drive_motor_1_defect',
|
'cutting_drive_motor_1_defect',
|
||||||
'cutting_drive_motor_2_defect',
|
'cutting_drive_motor_2_defect',
|
||||||
'cutting_drive_motor_3_defect',
|
'cutting_drive_motor_3_defect',
|
||||||
'cutting_height_blocked',
|
'cutting_height_blocked',
|
||||||
'cutting_height_problem',
|
|
||||||
'cutting_height_problem_curr',
|
'cutting_height_problem_curr',
|
||||||
'cutting_height_problem_dir',
|
'cutting_height_problem_dir',
|
||||||
'cutting_height_problem_drive',
|
'cutting_height_problem_drive',
|
||||||
|
'cutting_height_problem',
|
||||||
'cutting_motor_problem',
|
'cutting_motor_problem',
|
||||||
'cutting_stopped_slope_too_steep',
|
'cutting_stopped_slope_too_steep',
|
||||||
'cutting_system_blocked',
|
'cutting_system_blocked',
|
||||||
'cutting_system_blocked',
|
|
||||||
'cutting_system_imbalance_warning',
|
'cutting_system_imbalance_warning',
|
||||||
'cutting_system_major_imbalance',
|
'cutting_system_major_imbalance',
|
||||||
'destination_not_reachable',
|
'destination_not_reachable',
|
||||||
@ -1682,13 +1625,9 @@
|
|||||||
'docking_sensor_defect',
|
'docking_sensor_defect',
|
||||||
'electronic_problem',
|
'electronic_problem',
|
||||||
'empty_battery',
|
'empty_battery',
|
||||||
'error',
|
|
||||||
'error_at_power_up',
|
|
||||||
'fatal_error',
|
|
||||||
'folding_cutting_deck_sensor_defect',
|
'folding_cutting_deck_sensor_defect',
|
||||||
'folding_sensor_activated',
|
'folding_sensor_activated',
|
||||||
'geofence_problem',
|
'geofence_problem',
|
||||||
'geofence_problem',
|
|
||||||
'gps_navigation_problem',
|
'gps_navigation_problem',
|
||||||
'guide_1_not_found',
|
'guide_1_not_found',
|
||||||
'guide_2_not_found',
|
'guide_2_not_found',
|
||||||
@ -1706,7 +1645,6 @@
|
|||||||
'lift_sensor_defect',
|
'lift_sensor_defect',
|
||||||
'lifted',
|
'lifted',
|
||||||
'limited_cutting_height_range',
|
'limited_cutting_height_range',
|
||||||
'limited_cutting_height_range',
|
|
||||||
'loop_sensor_defect',
|
'loop_sensor_defect',
|
||||||
'loop_sensor_problem_front',
|
'loop_sensor_problem_front',
|
||||||
'loop_sensor_problem_left',
|
'loop_sensor_problem_left',
|
||||||
@ -1719,6 +1657,7 @@
|
|||||||
'no_accurate_position_from_satellites',
|
'no_accurate_position_from_satellites',
|
||||||
'no_confirmed_position',
|
'no_confirmed_position',
|
||||||
'no_drive',
|
'no_drive',
|
||||||
|
'no_error',
|
||||||
'no_loop_signal',
|
'no_loop_signal',
|
||||||
'no_power_in_charging_station',
|
'no_power_in_charging_station',
|
||||||
'no_response_from_charger',
|
'no_response_from_charger',
|
||||||
@ -1729,9 +1668,6 @@
|
|||||||
'safety_function_faulty',
|
'safety_function_faulty',
|
||||||
'settings_restored',
|
'settings_restored',
|
||||||
'sim_card_locked',
|
'sim_card_locked',
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_locked',
|
|
||||||
'sim_card_not_found',
|
'sim_card_not_found',
|
||||||
'sim_card_requires_pin',
|
'sim_card_requires_pin',
|
||||||
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern',
|
||||||
@ -1741,13 +1677,6 @@
|
|||||||
'stuck_in_charging_station',
|
'stuck_in_charging_station',
|
||||||
'switch_cord_problem',
|
'switch_cord_problem',
|
||||||
'temporary_battery_problem',
|
'temporary_battery_problem',
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'temporary_battery_problem',
|
|
||||||
'tilt_sensor_problem',
|
'tilt_sensor_problem',
|
||||||
'too_high_discharge_current',
|
'too_high_discharge_current',
|
||||||
'too_high_internal_current',
|
'too_high_internal_current',
|
||||||
@ -1777,6 +1706,13 @@
|
|||||||
'wrong_loop_signal',
|
'wrong_loop_signal',
|
||||||
'wrong_pin_code',
|
'wrong_pin_code',
|
||||||
'zone_generator_problem',
|
'zone_generator_problem',
|
||||||
|
'error_at_power_up',
|
||||||
|
'error',
|
||||||
|
'fatal_error',
|
||||||
|
'off',
|
||||||
|
'stopped',
|
||||||
|
'wait_power_up',
|
||||||
|
'wait_updating',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_call_logger_set_level, async_fire_time_changed
|
||||||
|
|
||||||
HASS_NS = "unused.homeassistant"
|
HASS_NS = "unused.homeassistant"
|
||||||
COMPONENTS_NS = f"{HASS_NS}.components"
|
COMPONENTS_NS = f"{HASS_NS}.components"
|
||||||
@ -73,12 +73,9 @@ async def test_log_filtering(
|
|||||||
msg_test(filter_logger, True, "format string shouldfilter%s", "not")
|
msg_test(filter_logger, True, "format string shouldfilter%s", "not")
|
||||||
|
|
||||||
# Filtering should work even if log level is modified
|
# Filtering should work even if log level is modified
|
||||||
await hass.services.async_call(
|
async with async_call_logger_set_level(
|
||||||
"logger",
|
"test.filter", "WARNING", hass=hass, caplog=caplog
|
||||||
"set_level",
|
):
|
||||||
{"test.filter": "warning"},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert filter_logger.getEffectiveLevel() == logging.WARNING
|
assert filter_logger.getEffectiveLevel() == logging.WARNING
|
||||||
msg_test(
|
msg_test(
|
||||||
filter_logger,
|
filter_logger,
|
||||||
@ -88,7 +85,9 @@ async def test_log_filtering(
|
|||||||
|
|
||||||
# Filtering should be scoped to a service
|
# Filtering should be scoped to a service
|
||||||
msg_test(
|
msg_test(
|
||||||
filter_logger, True, "this line containing otherfilterer should not be filtered"
|
filter_logger,
|
||||||
|
True,
|
||||||
|
"this line containing otherfilterer should not be filtered",
|
||||||
)
|
)
|
||||||
msg_test(
|
msg_test(
|
||||||
logging.getLogger("test.other_filter"),
|
logging.getLogger("test.other_filter"),
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant import loader
|
from homeassistant import loader
|
||||||
from homeassistant.components.logger.helpers import async_get_domain_config
|
from homeassistant.components.logger.helpers import DATA_LOGGER
|
||||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -76,7 +76,7 @@ async def test_integration_log_level(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.DEBUG
|
"homeassistant.components.websocket_api": logging.DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ async def test_custom_integration_log_level(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.hue": logging.DEBUG,
|
"homeassistant.components.hue": logging.DEBUG,
|
||||||
"custom_components.hue": logging.DEBUG,
|
"custom_components.hue": logging.DEBUG,
|
||||||
"some_other_logger": logging.DEBUG,
|
"some_other_logger": logging.DEBUG,
|
||||||
@ -182,7 +182,7 @@ async def test_module_log_level(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.DEBUG,
|
"homeassistant.components.websocket_api": logging.DEBUG,
|
||||||
"homeassistant.components.other_component": logging.WARNING,
|
"homeassistant.components.other_component": logging.WARNING,
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ async def test_module_log_level_override(
|
|||||||
{"logger": {"logs": {"homeassistant.components.websocket_api": "warning"}}},
|
{"logger": {"logs": {"homeassistant.components.websocket_api": "warning"}}},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.WARNING
|
"homeassistant.components.websocket_api": logging.WARNING
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +218,7 @@ async def test_module_log_level_override(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.ERROR
|
"homeassistant.components.websocket_api": logging.ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ async def test_module_log_level_override(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.DEBUG
|
"homeassistant.components.websocket_api": logging.DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,6 +256,6 @@ async def test_module_log_level_override(
|
|||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
|
||||||
assert async_get_domain_config(hass).overrides == {
|
assert hass.data[DATA_LOGGER].overrides == {
|
||||||
"homeassistant.components.websocket_api": logging.NOTSET
|
"homeassistant.components.websocket_api": logging.NOTSET
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from aionut import NUTError, NUTLoginError
|
from aionut import NUTError, NUTLoginError
|
||||||
|
|
||||||
from homeassistant import config_entries, setup
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.nut.config_flow import PASSWORD_NOT_CHANGED
|
from homeassistant.components.nut.config_flow import PASSWORD_NOT_CHANGED
|
||||||
from homeassistant.components.nut.const import DOMAIN
|
from homeassistant.components.nut.const import DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -86,7 +86,6 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
async def test_form_user_one_alias(hass: HomeAssistant) -> None:
|
async def test_form_user_one_alias(hass: HomeAssistant) -> None:
|
||||||
"""Test we can configure a device with one alias."""
|
"""Test we can configure a device with one alias."""
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -131,8 +130,6 @@ async def test_form_user_one_alias(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
async def test_form_user_multiple_aliases(hass: HomeAssistant) -> None:
|
async def test_form_user_multiple_aliases(hass: HomeAssistant) -> None:
|
||||||
"""Test we can configure device with multiple aliases."""
|
"""Test we can configure device with multiple aliases."""
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
|
||||||
|
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={CONF_HOST: "2.2.2.2", CONF_PORT: 123, CONF_RESOURCES: ["battery.charge"]},
|
data={CONF_HOST: "2.2.2.2", CONF_PORT: 123, CONF_RESOURCES: ["battery.charge"]},
|
||||||
@ -202,7 +199,6 @@ async def test_form_user_one_alias_with_ignored_entry(hass: HomeAssistant) -> No
|
|||||||
)
|
)
|
||||||
ignored_entry.add_to_hass(hass)
|
ignored_entry.add_to_hass(hass)
|
||||||
|
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
@ -18,10 +18,12 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .util import _get_mock_nutclient, async_init_integration
|
from .util import _get_mock_nutclient, async_init_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_migrations(hass: HomeAssistant) -> None:
|
async def test_config_entry_migrations(hass: HomeAssistant) -> None:
|
||||||
@ -84,6 +86,78 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
|||||||
assert not hass.data.get(DOMAIN)
|
assert not hass.data.get(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_device_valid(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that we cannot remove a device that still exists."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
|
mock_serial_number = "A00000000000"
|
||||||
|
config_entry = await async_init_integration(
|
||||||
|
hass,
|
||||||
|
username="someuser",
|
||||||
|
password="somepassword",
|
||||||
|
list_vars={"ups.serial": mock_serial_number},
|
||||||
|
list_ups={"ups1": "UPS 1"},
|
||||||
|
list_commands_return_value=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
assert device_registry is not None
|
||||||
|
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, mock_serial_number)}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert device_entry is not None
|
||||||
|
assert device_entry.serial_number == mock_serial_number
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
response = await client.remove_device(device_entry.id, config_entry.entry_id)
|
||||||
|
assert not response["success"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_device_stale(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that we can remove a device that no longer exists."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
|
mock_serial_number = "A00000000000"
|
||||||
|
config_entry = await async_init_integration(
|
||||||
|
hass,
|
||||||
|
username="someuser",
|
||||||
|
password="somepassword",
|
||||||
|
list_vars={"ups.serial": mock_serial_number},
|
||||||
|
list_ups={"ups1": "UPS 1"},
|
||||||
|
list_commands_return_value=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
assert device_registry is not None
|
||||||
|
|
||||||
|
device_entry = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, "remove-device-id")},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert device_entry is not None
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
response = await client.remove_device(device_entry.id, config_entry.entry_id)
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
# Verify that device entry is removed
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "remove-device-id")}
|
||||||
|
)
|
||||||
|
assert device_entry is None
|
||||||
|
|
||||||
|
|
||||||
async def test_config_not_ready(
|
async def test_config_not_ready(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
@ -125,7 +125,6 @@ async def test_pdu_devices_with_unique_ids(
|
|||||||
_test_sensor_and_attributes(
|
_test_sensor_and_attributes(
|
||||||
hass,
|
hass,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
model,
|
|
||||||
unique_id=f"{unique_id_base}_input.voltage",
|
unique_id=f"{unique_id_base}_input.voltage",
|
||||||
device_id="sensor.ups1_input_voltage",
|
device_id="sensor.ups1_input_voltage",
|
||||||
state_value="122.91",
|
state_value="122.91",
|
||||||
@ -140,7 +139,6 @@ async def test_pdu_devices_with_unique_ids(
|
|||||||
_test_sensor_and_attributes(
|
_test_sensor_and_attributes(
|
||||||
hass,
|
hass,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
model,
|
|
||||||
unique_id=f"{unique_id_base}_ambient.humidity.status",
|
unique_id=f"{unique_id_base}_ambient.humidity.status",
|
||||||
device_id="sensor.ups1_ambient_humidity_status",
|
device_id="sensor.ups1_ambient_humidity_status",
|
||||||
state_value="good",
|
state_value="good",
|
||||||
@ -153,7 +151,6 @@ async def test_pdu_devices_with_unique_ids(
|
|||||||
_test_sensor_and_attributes(
|
_test_sensor_and_attributes(
|
||||||
hass,
|
hass,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
model,
|
|
||||||
unique_id=f"{unique_id_base}_ambient.temperature.status",
|
unique_id=f"{unique_id_base}_ambient.temperature.status",
|
||||||
device_id="sensor.ups1_ambient_temperature_status",
|
device_id="sensor.ups1_ambient_temperature_status",
|
||||||
state_value="good",
|
state_value="good",
|
||||||
@ -334,7 +331,6 @@ async def test_pdu_dynamic_outlets(
|
|||||||
_test_sensor_and_attributes(
|
_test_sensor_and_attributes(
|
||||||
hass,
|
hass,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
model,
|
|
||||||
unique_id=f"{unique_id_base}_outlet.1.current",
|
unique_id=f"{unique_id_base}_outlet.1.current",
|
||||||
device_id="sensor.ups1_outlet_a1_current",
|
device_id="sensor.ups1_outlet_a1_current",
|
||||||
state_value="0",
|
state_value="0",
|
||||||
@ -348,7 +344,6 @@ async def test_pdu_dynamic_outlets(
|
|||||||
_test_sensor_and_attributes(
|
_test_sensor_and_attributes(
|
||||||
hass,
|
hass,
|
||||||
entity_registry,
|
entity_registry,
|
||||||
model,
|
|
||||||
unique_id=f"{unique_id_base}_outlet.24.current",
|
unique_id=f"{unique_id_base}_outlet.24.current",
|
||||||
device_id="sensor.ups1_outlet_a24_current",
|
device_id="sensor.ups1_outlet_a24_current",
|
||||||
state_value="0.19",
|
state_value="0.19",
|
||||||
|
@ -43,7 +43,7 @@ async def async_init_integration(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
ups_fixture: str | None = None,
|
ups_fixture: str | None = None,
|
||||||
host: str = "mock",
|
host: str = "mock",
|
||||||
port: str = "mock",
|
port: int = 1234,
|
||||||
username: str = "mock",
|
username: str = "mock",
|
||||||
password: str = "mock",
|
password: str = "mock",
|
||||||
alias: str | None = None,
|
alias: str | None = None,
|
||||||
@ -104,7 +104,6 @@ async def async_init_integration(
|
|||||||
def _test_sensor_and_attributes(
|
def _test_sensor_and_attributes(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
model: str,
|
|
||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
state_value: str,
|
state_value: str,
|
||||||
|
@ -40,6 +40,7 @@ TEST_GATEWAY_ID3 = "SOMFY_PROTECT-v0NT53occUBPyuJRzx59kalW1hFfzimN"
|
|||||||
|
|
||||||
TEST_HOST = "gateway-1234-5678-9123.local:8443"
|
TEST_HOST = "gateway-1234-5678-9123.local:8443"
|
||||||
TEST_HOST2 = "192.168.11.104:8443"
|
TEST_HOST2 = "192.168.11.104:8443"
|
||||||
|
TEST_TOKEN = "1234123412341234"
|
||||||
|
|
||||||
MOCK_GATEWAY_RESPONSE = [Mock(id=TEST_GATEWAY_ID)]
|
MOCK_GATEWAY_RESPONSE = [Mock(id=TEST_GATEWAY_ID)]
|
||||||
MOCK_GATEWAY2_RESPONSE = [Mock(id=TEST_GATEWAY_ID3), Mock(id=TEST_GATEWAY_ID2)]
|
MOCK_GATEWAY2_RESPONSE = [Mock(id=TEST_GATEWAY_ID3), Mock(id=TEST_GATEWAY_ID2)]
|
||||||
@ -152,7 +153,7 @@ async def test_form_only_cloud_supported(
|
|||||||
async def test_form_local_happy_flow(
|
async def test_form_local_happy_flow(
|
||||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form."""
|
"""Test local API configuration flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -179,21 +180,27 @@ async def test_form_local_happy_flow(
|
|||||||
"pyoverkiz.client.OverkizClient",
|
"pyoverkiz.client.OverkizClient",
|
||||||
login=AsyncMock(return_value=True),
|
login=AsyncMock(return_value=True),
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
get_setup_option=AsyncMock(return_value=True),
|
|
||||||
generate_local_token=AsyncMock(return_value="1234123412341234"),
|
|
||||||
activate_local_token=AsyncMock(return_value=True),
|
|
||||||
):
|
):
|
||||||
await hass.config_entries.flow.async_configure(
|
result4 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
"username": TEST_EMAIL,
|
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"host": "gateway-1234-5678-1234.local:8443",
|
"host": "gateway-1234-5678-1234.local:8443",
|
||||||
|
"token": TEST_TOKEN,
|
||||||
|
"verify_ssl": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result4["title"] == "gateway-1234-5678-1234.local:8443"
|
||||||
|
assert result4["data"] == {
|
||||||
|
"host": "gateway-1234-5678-1234.local:8443",
|
||||||
|
"token": TEST_TOKEN,
|
||||||
|
"verify_ssl": True,
|
||||||
|
"hub": TEST_SERVER,
|
||||||
|
"api_type": "local",
|
||||||
|
}
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -262,7 +269,7 @@ async def test_form_invalid_auth_cloud(
|
|||||||
(MaintenanceException, "server_in_maintenance"),
|
(MaintenanceException, "server_in_maintenance"),
|
||||||
(TooManyAttemptsBannedException, "too_many_attempts"),
|
(TooManyAttemptsBannedException, "too_many_attempts"),
|
||||||
(UnknownUserException, "unsupported_hardware"),
|
(UnknownUserException, "unsupported_hardware"),
|
||||||
(NotSuchTokenException, "no_such_token"),
|
(NotSuchTokenException, "invalid_auth"),
|
||||||
(Exception, "unknown"),
|
(Exception, "unknown"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -297,8 +304,7 @@ async def test_form_invalid_auth_local(
|
|||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
"host": TEST_HOST,
|
"host": TEST_HOST,
|
||||||
"username": TEST_EMAIL,
|
"token": TEST_TOKEN,
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"verify_ssl": True,
|
"verify_ssl": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -309,52 +315,6 @@ async def test_form_invalid_auth_local(
|
|||||||
assert result4["errors"] == {"base": error}
|
assert result4["errors"] == {"base": error}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_local_developer_mode_disabled(
|
|
||||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
|
||||||
) -> None:
|
|
||||||
"""Test we get the form."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{"hub": TEST_SERVER},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.FORM
|
|
||||||
assert result2["step_id"] == "local_or_cloud"
|
|
||||||
|
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{"api_type": "local"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result3["type"] is FlowResultType.FORM
|
|
||||||
assert result3["step_id"] == "local"
|
|
||||||
|
|
||||||
with patch.multiple(
|
|
||||||
"pyoverkiz.client.OverkizClient",
|
|
||||||
login=AsyncMock(return_value=True),
|
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
|
||||||
get_setup_option=AsyncMock(return_value=None),
|
|
||||||
):
|
|
||||||
result4 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
"username": TEST_EMAIL,
|
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"host": "gateway-1234-5678-1234.local:8443",
|
|
||||||
"verify_ssl": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result4["type"] is FlowResultType.FORM
|
|
||||||
assert result4["errors"] == {"base": "developer_mode_disabled"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("side_effect", "error"),
|
("side_effect", "error"),
|
||||||
[
|
[
|
||||||
@ -444,16 +404,18 @@ async def test_cloud_abort_on_duplicate_entry(
|
|||||||
async def test_local_abort_on_duplicate_entry(
|
async def test_local_abort_on_duplicate_entry(
|
||||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form."""
|
"""Test local API configuration is aborted if gateway already exists."""
|
||||||
|
|
||||||
MockConfigEntry(
|
MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
unique_id=TEST_GATEWAY_ID,
|
unique_id=TEST_GATEWAY_ID,
|
||||||
|
version=2,
|
||||||
data={
|
data={
|
||||||
"host": TEST_HOST,
|
"host": TEST_HOST,
|
||||||
"username": TEST_EMAIL,
|
"token": TEST_TOKEN,
|
||||||
"password": TEST_PASSWORD,
|
"verify_ssl": True,
|
||||||
"hub": TEST_SERVER,
|
"hub": TEST_SERVER,
|
||||||
|
"api_type": "local",
|
||||||
},
|
},
|
||||||
).add_to_hass(hass)
|
).add_to_hass(hass)
|
||||||
|
|
||||||
@ -484,15 +446,12 @@ async def test_local_abort_on_duplicate_entry(
|
|||||||
login=AsyncMock(return_value=True),
|
login=AsyncMock(return_value=True),
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
get_setup_option=AsyncMock(return_value=True),
|
get_setup_option=AsyncMock(return_value=True),
|
||||||
generate_local_token=AsyncMock(return_value="1234123412341234"),
|
|
||||||
activate_local_token=AsyncMock(return_value=True),
|
|
||||||
):
|
):
|
||||||
result4 = await hass.config_entries.flow.async_configure(
|
result4 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
"host": TEST_HOST,
|
"host": TEST_HOST,
|
||||||
"username": TEST_EMAIL,
|
"token": TEST_TOKEN,
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"verify_ssl": True,
|
"verify_ssl": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -639,18 +598,18 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None:
|
|||||||
assert result2["reason"] == "reauth_wrong_account"
|
assert result2["reason"] == "reauth_wrong_account"
|
||||||
|
|
||||||
|
|
||||||
async def test_local_reauth_success(hass: HomeAssistant) -> None:
|
async def test_local_reauth_legacy(hass: HomeAssistant) -> None:
|
||||||
"""Test reauthentication flow."""
|
"""Test legacy reauthentication flow with username/password."""
|
||||||
|
|
||||||
mock_entry = MockConfigEntry(
|
mock_entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
unique_id=TEST_GATEWAY_ID,
|
unique_id=TEST_GATEWAY_ID,
|
||||||
version=2,
|
version=2,
|
||||||
data={
|
data={
|
||||||
|
"host": TEST_HOST,
|
||||||
"username": TEST_EMAIL,
|
"username": TEST_EMAIL,
|
||||||
"password": TEST_PASSWORD,
|
"password": TEST_PASSWORD,
|
||||||
|
"verify_ssl": True,
|
||||||
"hub": TEST_SERVER,
|
"hub": TEST_SERVER,
|
||||||
"host": TEST_HOST,
|
|
||||||
"api_type": "local",
|
"api_type": "local",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -672,36 +631,85 @@ async def test_local_reauth_success(hass: HomeAssistant) -> None:
|
|||||||
"pyoverkiz.client.OverkizClient",
|
"pyoverkiz.client.OverkizClient",
|
||||||
login=AsyncMock(return_value=True),
|
login=AsyncMock(return_value=True),
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
get_setup_option=AsyncMock(return_value=True),
|
|
||||||
generate_local_token=AsyncMock(return_value="1234123412341234"),
|
|
||||||
activate_local_token=AsyncMock(return_value=True),
|
|
||||||
):
|
):
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={
|
{
|
||||||
"username": TEST_EMAIL,
|
"host": TEST_HOST,
|
||||||
"password": TEST_PASSWORD2,
|
"token": "new_token",
|
||||||
|
"verify_ssl": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result3["type"] is FlowResultType.ABORT
|
assert result3["type"] is FlowResultType.ABORT
|
||||||
assert result3["reason"] == "reauth_successful"
|
assert result3["reason"] == "reauth_successful"
|
||||||
assert mock_entry.data["username"] == TEST_EMAIL
|
assert mock_entry.data["host"] == TEST_HOST
|
||||||
assert mock_entry.data["password"] == TEST_PASSWORD2
|
assert mock_entry.data["token"] == "new_token"
|
||||||
|
assert mock_entry.data["verify_ssl"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_local_reauth_success(hass: HomeAssistant) -> None:
|
||||||
|
"""Test modern local reauth flow."""
|
||||||
|
mock_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=TEST_GATEWAY_ID,
|
||||||
|
version=2,
|
||||||
|
data={
|
||||||
|
"host": TEST_HOST,
|
||||||
|
"token": "old_token",
|
||||||
|
"verify_ssl": True,
|
||||||
|
"hub": TEST_SERVER,
|
||||||
|
"api_type": "local",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await mock_entry.start_reauth_flow(hass)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "local_or_cloud"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"api_type": "local"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["step_id"] == "local"
|
||||||
|
|
||||||
|
with patch.multiple(
|
||||||
|
"pyoverkiz.client.OverkizClient",
|
||||||
|
login=AsyncMock(return_value=True),
|
||||||
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
|
):
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
"host": TEST_HOST,
|
||||||
|
"token": "new_token",
|
||||||
|
"verify_ssl": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3["type"] is FlowResultType.ABORT
|
||||||
|
assert result3["reason"] == "reauth_successful"
|
||||||
|
assert mock_entry.data["host"] == TEST_HOST
|
||||||
|
assert mock_entry.data["token"] == "new_token"
|
||||||
|
assert mock_entry.data["verify_ssl"] is True
|
||||||
|
assert "username" not in mock_entry.data
|
||||||
|
assert "password" not in mock_entry.data
|
||||||
|
|
||||||
|
|
||||||
async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None:
|
async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None:
|
||||||
"""Test reauthentication flow."""
|
"""Test local reauth flow with wrong gateway account."""
|
||||||
|
|
||||||
mock_entry = MockConfigEntry(
|
mock_entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
unique_id=TEST_GATEWAY_ID2,
|
unique_id=TEST_GATEWAY_ID2,
|
||||||
version=2,
|
version=2,
|
||||||
data={
|
data={
|
||||||
"username": TEST_EMAIL,
|
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"hub": TEST_SERVER,
|
|
||||||
"host": TEST_HOST,
|
"host": TEST_HOST,
|
||||||
|
"token": "old_token",
|
||||||
|
"verify_ssl": True,
|
||||||
|
"hub": TEST_SERVER,
|
||||||
"api_type": "local",
|
"api_type": "local",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -722,15 +730,13 @@ async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None:
|
|||||||
"pyoverkiz.client.OverkizClient",
|
"pyoverkiz.client.OverkizClient",
|
||||||
login=AsyncMock(return_value=True),
|
login=AsyncMock(return_value=True),
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
get_setup_option=AsyncMock(return_value=True),
|
|
||||||
generate_local_token=AsyncMock(return_value="1234123412341234"),
|
|
||||||
activate_local_token=AsyncMock(return_value=True),
|
|
||||||
):
|
):
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={
|
{
|
||||||
"username": TEST_EMAIL,
|
"host": TEST_HOST,
|
||||||
"password": TEST_PASSWORD2,
|
"token": "new_token",
|
||||||
|
"verify_ssl": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -897,27 +903,27 @@ async def test_local_zeroconf_flow(
|
|||||||
"pyoverkiz.client.OverkizClient",
|
"pyoverkiz.client.OverkizClient",
|
||||||
login=AsyncMock(return_value=True),
|
login=AsyncMock(return_value=True),
|
||||||
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE),
|
||||||
get_setup_option=AsyncMock(return_value=True),
|
|
||||||
generate_local_token=AsyncMock(return_value="1234123412341234"),
|
|
||||||
activate_local_token=AsyncMock(return_value=True),
|
|
||||||
):
|
):
|
||||||
result4 = await hass.config_entries.flow.async_configure(
|
result4 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{"username": TEST_EMAIL, "password": TEST_PASSWORD, "verify_ssl": False},
|
{
|
||||||
|
"host": "gateway-1234-5678-9123.local:8443",
|
||||||
|
"token": TEST_TOKEN,
|
||||||
|
"verify_ssl": False,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result4["title"] == "gateway-1234-5678-9123.local:8443"
|
assert result4["title"] == "gateway-1234-5678-9123.local:8443"
|
||||||
assert result4["data"] == {
|
|
||||||
"username": TEST_EMAIL,
|
|
||||||
"password": TEST_PASSWORD,
|
|
||||||
"hub": TEST_SERVER,
|
|
||||||
"host": "gateway-1234-5678-9123.local:8443",
|
|
||||||
"api_type": "local",
|
|
||||||
"token": "1234123412341234",
|
|
||||||
"verify_ssl": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Verify no username/password in data
|
||||||
|
assert result4["data"] == {
|
||||||
|
"host": "gateway-1234-5678-9123.local:8443",
|
||||||
|
"token": TEST_TOKEN,
|
||||||
|
"verify_ssl": False,
|
||||||
|
"hub": TEST_SERVER,
|
||||||
|
"api_type": "local",
|
||||||
|
}
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.smarty import DOMAIN
|
from homeassistant.components.smarty.const import DOMAIN
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from homeassistant.components.smarty.const import DOMAIN
|
from homeassistant.components.smarty.const import DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
@ -114,52 +114,3 @@ async def test_existing_entry(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_import_flow(
|
|
||||||
hass: HomeAssistant, mock_smarty: AsyncMock, mock_setup_entry: AsyncMock
|
|
||||||
) -> None:
|
|
||||||
"""Test the import flow."""
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data={CONF_HOST: "192.168.0.2", CONF_NAME: "Smarty"},
|
|
||||||
)
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
||||||
assert result["title"] == "Smarty"
|
|
||||||
assert result["data"] == {CONF_HOST: "192.168.0.2"}
|
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_cannot_connect(
|
|
||||||
hass: HomeAssistant, mock_smarty: AsyncMock
|
|
||||||
) -> None:
|
|
||||||
"""Test we handle cannot connect error."""
|
|
||||||
|
|
||||||
mock_smarty.update.return_value = False
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data={CONF_HOST: "192.168.0.2", CONF_NAME: "Smarty"},
|
|
||||||
)
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
|
||||||
assert result["reason"] == "cannot_connect"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_unknown_error(
|
|
||||||
hass: HomeAssistant, mock_smarty: AsyncMock
|
|
||||||
) -> None:
|
|
||||||
"""Test we handle unknown error."""
|
|
||||||
|
|
||||||
mock_smarty.update.side_effect = Exception
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data={CONF_HOST: "192.168.0.2", CONF_NAME: "Smarty"},
|
|
||||||
)
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
|
||||||
assert result["reason"] == "unknown"
|
|
||||||
|
@ -4,68 +4,15 @@ from unittest.mock import AsyncMock
|
|||||||
|
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.smarty import DOMAIN
|
from homeassistant.components.smarty.const import DOMAIN
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from . import setup_integration
|
from . import setup_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_import_flow(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_smarty: AsyncMock,
|
|
||||||
issue_registry: ir.IssueRegistry,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test import flow."""
|
|
||||||
assert await async_setup_component(
|
|
||||||
hass, DOMAIN, {DOMAIN: {CONF_HOST: "192.168.0.2", CONF_NAME: "smarty"}}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
|
||||||
assert (HOMEASSISTANT_DOMAIN, "deprecated_yaml_smarty") in issue_registry.issues
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_flow_already_exists(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_smarty: AsyncMock,
|
|
||||||
issue_registry: ir.IssueRegistry,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
mock_config_entry: MockConfigEntry,
|
|
||||||
) -> None:
|
|
||||||
"""Test import flow when entry already exists."""
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
assert await async_setup_component(
|
|
||||||
hass, DOMAIN, {DOMAIN: {CONF_HOST: "192.168.0.2", CONF_NAME: "smarty"}}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
|
||||||
assert (HOMEASSISTANT_DOMAIN, "deprecated_yaml_smarty") in issue_registry.issues
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_flow_error(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_smarty: AsyncMock,
|
|
||||||
issue_registry: ir.IssueRegistry,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test import flow when error occurs."""
|
|
||||||
mock_smarty.update.return_value = False
|
|
||||||
assert await async_setup_component(
|
|
||||||
hass, DOMAIN, {DOMAIN: {CONF_HOST: "192.168.0.2", CONF_NAME: "smarty"}}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
|
||||||
assert (
|
|
||||||
DOMAIN,
|
|
||||||
"deprecated_yaml_import_issue_cannot_connect",
|
|
||||||
) in issue_registry.issues
|
|
||||||
|
|
||||||
|
|
||||||
async def test_device(
|
async def test_device(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
|
@ -463,3 +463,28 @@ HUMIDIFIER_SERVICE_INFO = BluetoothServiceInfoBleak(
|
|||||||
connectable=True,
|
connectable=True,
|
||||||
tx_power=-127,
|
tx_power=-127,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WOSTRIP_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
|
name="WoStrip",
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
manufacturer_data={
|
||||||
|
2409: b'\x84\xf7\x03\xb3?\xde\x04\xe4"\x0c\x00\x00\x00\x00\x00\x00'
|
||||||
|
},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"r\x00d"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
rssi=-60,
|
||||||
|
source="local",
|
||||||
|
advertisement=generate_advertisement_data(
|
||||||
|
local_name="WoStrip",
|
||||||
|
manufacturer_data={
|
||||||
|
2409: b'\x84\xf7\x03\xb3?\xde\x04\xe4"\x0c\x00\x00\x00\x00\x00\x00'
|
||||||
|
},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"r\x00d"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
),
|
||||||
|
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "WoStrip"),
|
||||||
|
time=0,
|
||||||
|
connectable=True,
|
||||||
|
tx_power=-127,
|
||||||
|
)
|
||||||
|
139
tests/components/switchbot/test_light.py
Normal file
139
tests/components/switchbot/test_light.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"""Test the switchbot lights."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from switchbot import ColorMode as switchbotColorMode
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
|
ATTR_RGB_COLOR,
|
||||||
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import WOSTRIP_SERVICE_INFO
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
(
|
||||||
|
"service",
|
||||||
|
"service_data",
|
||||||
|
"mock_method",
|
||||||
|
"expected_args",
|
||||||
|
"color_modes",
|
||||||
|
"color_mode",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{},
|
||||||
|
"turn_off",
|
||||||
|
(),
|
||||||
|
{switchbotColorMode.RGB},
|
||||||
|
switchbotColorMode.RGB,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{},
|
||||||
|
"turn_on",
|
||||||
|
(),
|
||||||
|
{switchbotColorMode.RGB},
|
||||||
|
switchbotColorMode.RGB,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_BRIGHTNESS: 128},
|
||||||
|
"set_brightness",
|
||||||
|
(round(128 / 255 * 100),),
|
||||||
|
{switchbotColorMode.RGB},
|
||||||
|
switchbotColorMode.RGB,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_RGB_COLOR: (255, 0, 0)},
|
||||||
|
"set_rgb",
|
||||||
|
(round(255 / 255 * 100), 255, 0, 0),
|
||||||
|
{switchbotColorMode.RGB},
|
||||||
|
switchbotColorMode.RGB,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_COLOR_TEMP_KELVIN: 4000},
|
||||||
|
"set_color_temp",
|
||||||
|
(100, 4000),
|
||||||
|
{switchbotColorMode.COLOR_TEMP},
|
||||||
|
switchbotColorMode.COLOR_TEMP,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_light_strip_services(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry_factory: Callable[[str], MockConfigEntry],
|
||||||
|
service: str,
|
||||||
|
service_data: dict,
|
||||||
|
mock_method: str,
|
||||||
|
expected_args: Any,
|
||||||
|
color_modes: set | None,
|
||||||
|
color_mode: switchbotColorMode | None,
|
||||||
|
) -> None:
|
||||||
|
"""Test all SwitchBot light strip services with proper parameters."""
|
||||||
|
inject_bluetooth_service_info(hass, WOSTRIP_SERVICE_INFO)
|
||||||
|
|
||||||
|
entry = mock_entry_factory(sensor_type="light_strip")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
entity_id = "light.test_name"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("switchbot.SwitchbotLightStrip.color_modes", new=color_modes),
|
||||||
|
patch("switchbot.SwitchbotLightStrip.color_mode", new=color_mode),
|
||||||
|
patch(
|
||||||
|
"switchbot.SwitchbotLightStrip.turn_on",
|
||||||
|
new=AsyncMock(return_value=True),
|
||||||
|
) as mock_turn_on,
|
||||||
|
patch(
|
||||||
|
"switchbot.SwitchbotLightStrip.turn_off",
|
||||||
|
new=AsyncMock(return_value=True),
|
||||||
|
) as mock_turn_off,
|
||||||
|
patch(
|
||||||
|
"switchbot.SwitchbotLightStrip.set_brightness",
|
||||||
|
new=AsyncMock(return_value=True),
|
||||||
|
) as mock_set_brightness,
|
||||||
|
patch(
|
||||||
|
"switchbot.SwitchbotLightStrip.set_rgb",
|
||||||
|
new=AsyncMock(return_value=True),
|
||||||
|
) as mock_set_rgb,
|
||||||
|
patch(
|
||||||
|
"switchbot.SwitchbotLightStrip.set_color_temp",
|
||||||
|
new=AsyncMock(return_value=True),
|
||||||
|
) as mock_set_color_temp,
|
||||||
|
patch("switchbot.SwitchbotLightStrip.update", new=AsyncMock(return_value=None)),
|
||||||
|
):
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
service,
|
||||||
|
{**service_data, ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_map = {
|
||||||
|
"turn_off": mock_turn_off,
|
||||||
|
"turn_on": mock_turn_on,
|
||||||
|
"set_brightness": mock_set_brightness,
|
||||||
|
"set_rgb": mock_set_rgb,
|
||||||
|
"set_color_temp": mock_set_color_temp,
|
||||||
|
}
|
||||||
|
mock_instance = mock_map[mock_method]
|
||||||
|
mock_instance.assert_awaited_once_with(*expected_args)
|
@ -88,7 +88,7 @@
|
|||||||
}),
|
}),
|
||||||
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
|
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
|
||||||
'original_icon': 'mdi:battery-unknown',
|
'original_icon': 'mdi:battery-unknown',
|
||||||
'original_name': 'Off grid reserve',
|
'original_name': 'Off-grid reserve',
|
||||||
'platform': 'tesla_fleet',
|
'platform': 'tesla_fleet',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'supported_features': 0,
|
'supported_features': 0,
|
||||||
@ -101,7 +101,7 @@
|
|||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'device_class': 'battery',
|
'device_class': 'battery',
|
||||||
'friendly_name': 'Energy Site Off grid reserve',
|
'friendly_name': 'Energy Site Off-grid reserve',
|
||||||
'icon': 'mdi:battery-unknown',
|
'icon': 'mdi:battery-unknown',
|
||||||
'max': 100,
|
'max': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user