mirror of
https://github.com/home-assistant/core.git
synced 2026-02-28 04:51:41 +01:00
Compare commits
71 Commits
epenet/202
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642864959a | ||
|
|
7ef6c34149 | ||
|
|
5b32e42b8c | ||
|
|
1be8b8e525 | ||
|
|
3fae15c430 | ||
|
|
c7e78568d0 | ||
|
|
492b542136 | ||
|
|
0f4852d8c2 | ||
|
|
737c0c1823 | ||
|
|
5fadcb01e9 | ||
|
|
2b4f46a739 | ||
|
|
44fe37da1f | ||
|
|
abd4e89577 | ||
|
|
033798835a | ||
|
|
83c77957c1 | ||
|
|
b1bc1dc102 | ||
|
|
40b8a2c380 | ||
|
|
fb23a6fbf8 | ||
|
|
faad3de02c | ||
|
|
5f30f532e5 | ||
|
|
667e8c4d38 | ||
|
|
74240ecd26 | ||
|
|
c81ee53265 | ||
|
|
8835f1d5e6 | ||
|
|
2ca84182d8 | ||
|
|
3f0d1bc071 | ||
|
|
350f462bdf | ||
|
|
2f98e68ed8 | ||
|
|
5b7fac94e5 | ||
|
|
c32ce3da5c | ||
|
|
0e1d1fbaed | ||
|
|
57d7f364f4 | ||
|
|
7cc5777b47 | ||
|
|
5e3f23b6a2 | ||
|
|
6873a40407 | ||
|
|
ddaa2fb293 | ||
|
|
53b6223459 | ||
|
|
7329cfb927 | ||
|
|
44b80dde0c | ||
|
|
8c125e4e4f | ||
|
|
227a258382 | ||
|
|
addc2a6766 | ||
|
|
97bcea9727 | ||
|
|
4f05c807b0 | ||
|
|
177a918c26 | ||
|
|
9705770c6c | ||
|
|
7309351165 | ||
|
|
d0401de70d | ||
|
|
6b89359a73 | ||
|
|
b31bafab99 | ||
|
|
84c556bb63 | ||
|
|
225ea02d9a | ||
|
|
ebd1cc994c | ||
|
|
9ec22ba158 | ||
|
|
2ff85d2134 | ||
|
|
3eb7f04510 | ||
|
|
54613ac8d9 | ||
|
|
044522a8ab | ||
|
|
19bf41496a | ||
|
|
a7efba098d | ||
|
|
042ad3b759 | ||
|
|
4270e4c793 | ||
|
|
cb11c22e76 | ||
|
|
c6e23fec93 | ||
|
|
553cecb397 | ||
|
|
bb7d5897d1 | ||
|
|
3e050ebe59 | ||
|
|
856a9e695a | ||
|
|
1944a8bd3a | ||
|
|
3f11af8084 | ||
|
|
46a87cd9dd |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -605,7 +605,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
|
||||
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -401,8 +401,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/duckdns/ @tr4nt0r
|
||||
/tests/components/duckdns/ @tr4nt0r
|
||||
/homeassistant/components/duke_energy/ @hunterjm
|
||||
/tests/components/duke_energy/ @hunterjm
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192
|
||||
@@ -1901,8 +1899,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/withings/ @joostlek
|
||||
/homeassistant/components/wiz/ @sbidy @arturpragacz
|
||||
/tests/components/wiz/ @sbidy @arturpragacz
|
||||
/homeassistant/components/wled/ @frenck
|
||||
/tests/components/wled/ @frenck
|
||||
/homeassistant/components/wled/ @frenck @mik-laj
|
||||
/tests/components/wled/ @frenck @mik-laj
|
||||
/homeassistant/components/wmspro/ @mback2k
|
||||
/tests/components/wmspro/ @mback2k
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
||||
|
||||
@@ -29,6 +29,9 @@ ATTR_NOTIFICATION_TYPE = "message"
|
||||
ATTR_REACTION = "reaction"
|
||||
ATTR_RECEIVER = "publicUserUuid"
|
||||
|
||||
SERVICE_PUSH_NOTIFICATION = "send_message"
|
||||
SERVICE_ACTIVITY_STREAM_REACTION = "send_reaction"
|
||||
|
||||
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
@@ -93,7 +96,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
async_send_activity_stream_reaction,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA,
|
||||
)
|
||||
@@ -101,7 +104,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"send_message",
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
entity_domain=TODO_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_NOTIFICATION_TYPE): vol.All(
|
||||
|
||||
@@ -64,6 +64,8 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: (
|
||||
data.sensor.total_energy.value
|
||||
if data.sensor.total_energy is not None
|
||||
|
||||
@@ -31,10 +31,6 @@ ATTR_FRIDAY_SLOTS = "friday_slots"
|
||||
ATTR_SATURDAY_SLOTS = "saturday_slots"
|
||||
ATTR_SUNDAY_SLOTS = "sunday_slots"
|
||||
|
||||
# Service names
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule"
|
||||
SERVICE_SYNC_TIME = "sync_time"
|
||||
|
||||
|
||||
# Schema for a single time slot
|
||||
_SLOT_SCHEMA = vol.Schema(
|
||||
@@ -260,14 +256,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the BSB-LAN services."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
set_hot_water_schedule,
|
||||
schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SYNC_TIME,
|
||||
"sync_time",
|
||||
async_sync_time,
|
||||
schema=SYNC_TIME_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -807,6 +807,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
# The lovelace app loops media to prevent timing out, don't show that
|
||||
if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE:
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
if (media_status := self._media_status()[0]) is not None:
|
||||
if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING:
|
||||
return MediaPlayerState.PLAYING
|
||||
@@ -817,19 +818,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
if media_status.player_is_idle:
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
# If library consider us idle, that is our off state
|
||||
# it takes HDMI status into account for cast devices.
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
|
||||
# Some apps don't report media status, show the player as playing
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
if self.app_id is not None:
|
||||
if self.app_id is not None and self.app_id != pychromecast.config.APP_BACKDROP:
|
||||
# We have an active app
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
# If library consider us idle, that is our off state
|
||||
# it takes HDMI status into account for cast devices.
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
"""The Duke Energy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import DukeEnergyConfigEntry, DukeEnergyCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
|
||||
"""Set up Duke Energy from a config entry."""
|
||||
|
||||
coordinator = DukeEnergyCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Config flow for Duke Energy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiodukeenergy import DukeEnergy
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Duke Energy."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
api = DukeEnergy(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session
|
||||
)
|
||||
try:
|
||||
auth = await api.authenticate()
|
||||
except ClientResponseError as e:
|
||||
errors["base"] = "invalid_auth" if e.status == 404 else "cannot_connect"
|
||||
except ClientError, TimeoutError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = auth["internalUserID"].lower()
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
email = auth["loginEmailAddress"].lower()
|
||||
data = {
|
||||
CONF_EMAIL: email,
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
}
|
||||
self._async_abort_entries_match(data)
|
||||
return self.async_create_entry(title=email, data=data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Constants for the Duke Energy integration."""
|
||||
|
||||
DOMAIN = "duke_energy"
|
||||
@@ -1,222 +0,0 @@
|
||||
"""Coordinator to handle Duke Energy connections."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiodukeenergy import DukeEnergy
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
StatisticData,
|
||||
StatisticMeanType,
|
||||
StatisticMetaData,
|
||||
)
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import EnergyConverter
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_SUPPORTED_METER_TYPES = ("ELECTRIC",)
|
||||
|
||||
type DukeEnergyConfigEntry = ConfigEntry[DukeEnergyCoordinator]
|
||||
|
||||
|
||||
class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Handle inserting statistics."""
|
||||
|
||||
config_entry: DukeEnergyConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: DukeEnergyConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the data handler."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="Duke Energy",
|
||||
# Data is updated daily on Duke Energy.
|
||||
# Refresh every 12h to be at most 12h behind.
|
||||
update_interval=timedelta(hours=12),
|
||||
)
|
||||
self.api = DukeEnergy(
|
||||
config_entry.data[CONF_USERNAME],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
self._statistic_ids: set = set()
|
||||
|
||||
@callback
|
||||
def _dummy_listener() -> None:
|
||||
pass
|
||||
|
||||
# Force the coordinator to periodically update by registering at least one listener.
|
||||
# Duke Energy does not provide forecast data, so all information is historical.
|
||||
# This makes _async_update_data get periodically called so we can insert statistics.
|
||||
self.async_add_listener(_dummy_listener)
|
||||
|
||||
self.config_entry.async_on_unload(self._clear_statistics)
|
||||
|
||||
def _clear_statistics(self) -> None:
|
||||
"""Clear statistics."""
|
||||
get_instance(self.hass).async_clear_statistics(list(self._statistic_ids))
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Insert Duke Energy statistics."""
|
||||
meters: dict[str, dict[str, Any]] = await self.api.get_meters()
|
||||
for serial_number, meter in meters.items():
|
||||
if (
|
||||
not isinstance(meter["serviceType"], str)
|
||||
or meter["serviceType"] not in _SUPPORTED_METER_TYPES
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Skipping unsupported meter type %s", meter["serviceType"]
|
||||
)
|
||||
continue
|
||||
|
||||
id_prefix = f"{meter['serviceType'].lower()}_{serial_number}"
|
||||
consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption"
|
||||
self._statistic_ids.add(consumption_statistic_id)
|
||||
_LOGGER.debug(
|
||||
"Updating Statistics for %s",
|
||||
consumption_statistic_id,
|
||||
)
|
||||
|
||||
last_stat = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
|
||||
)
|
||||
if not last_stat:
|
||||
_LOGGER.debug("Updating statistic for the first time")
|
||||
usage = await self._async_get_energy_usage(meter)
|
||||
consumption_sum = 0.0
|
||||
last_stats_time = None
|
||||
else:
|
||||
usage = await self._async_get_energy_usage(
|
||||
meter,
|
||||
last_stat[consumption_statistic_id][0]["start"],
|
||||
)
|
||||
if not usage:
|
||||
_LOGGER.debug("No recent usage data. Skipping update")
|
||||
continue
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
self.hass,
|
||||
min(usage.keys()),
|
||||
None,
|
||||
{consumption_statistic_id},
|
||||
"hour",
|
||||
None,
|
||||
{"sum"},
|
||||
)
|
||||
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
|
||||
last_stats_time = stats[consumption_statistic_id][0]["start"]
|
||||
|
||||
consumption_statistics = []
|
||||
|
||||
for start, data in usage.items():
|
||||
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||
continue
|
||||
consumption_sum += data["energy"]
|
||||
|
||||
consumption_statistics.append(
|
||||
StatisticData(
|
||||
start=start, state=data["energy"], sum=consumption_sum
|
||||
)
|
||||
)
|
||||
|
||||
name_prefix = (
|
||||
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
|
||||
)
|
||||
consumption_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"{name_prefix} Consumption",
|
||||
source=DOMAIN,
|
||||
statistic_id=consumption_statistic_id,
|
||||
unit_class=EnergyConverter.UNIT_CLASS,
|
||||
unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR
|
||||
if meter["serviceType"] == "ELECTRIC"
|
||||
else UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(consumption_statistics),
|
||||
consumption_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(
|
||||
self.hass, consumption_metadata, consumption_statistics
|
||||
)
|
||||
|
||||
async def _async_get_energy_usage(
|
||||
self, meter: dict[str, Any], start_time: float | None = None
|
||||
) -> dict[datetime, dict[str, float | int]]:
|
||||
"""Get energy usage.
|
||||
|
||||
If start_time is None, get usage since account activation (or as far back as possible),
|
||||
otherwise since start_time - 30 days to allow corrections in data.
|
||||
|
||||
Duke Energy provides hourly data all the way back to ~3 years.
|
||||
"""
|
||||
|
||||
# All of Duke Energy Service Areas are currently in America/New_York timezone
|
||||
# May need to re-think this if that ever changes and determine timezone based
|
||||
# on the service address somehow.
|
||||
tz = await dt_util.async_get_time_zone("America/New_York")
|
||||
lookback = timedelta(days=30)
|
||||
one = timedelta(days=1)
|
||||
if start_time is None:
|
||||
# Max 3 years of data
|
||||
start = dt_util.now(tz) - timedelta(days=3 * 365)
|
||||
else:
|
||||
start = datetime.fromtimestamp(start_time, tz=tz) - lookback
|
||||
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
|
||||
if agreement_date is not None:
|
||||
start = max(agreement_date.replace(tzinfo=tz), start)
|
||||
|
||||
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one
|
||||
_LOGGER.debug("Data lookup range: %s - %s", start, end)
|
||||
|
||||
start_step = max(end - lookback, start)
|
||||
end_step = end
|
||||
usage: dict[datetime, dict[str, float | int]] = {}
|
||||
while True:
|
||||
_LOGGER.debug("Getting hourly usage: %s - %s", start_step, end_step)
|
||||
try:
|
||||
# Get data
|
||||
results = await self.api.get_energy_usage(
|
||||
meter["serialNum"], "HOURLY", "DAY", start_step, end_step
|
||||
)
|
||||
usage = {**results["data"], **usage}
|
||||
|
||||
for missing in results["missing"]:
|
||||
_LOGGER.debug("Missing data: %s", missing)
|
||||
|
||||
# Set next range
|
||||
end_step = start_step - one
|
||||
start_step = max(start_step - lookback, start)
|
||||
|
||||
# Make sure we don't go back too far
|
||||
if end_step < start:
|
||||
break
|
||||
except TimeoutError, ClientError:
|
||||
# ClientError is raised when there is no more data for the range
|
||||
break
|
||||
|
||||
_LOGGER.debug("Got %s meter usage reads", len(usage))
|
||||
return usage
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "duke_energy",
|
||||
"name": "Duke Energy",
|
||||
"codeowners": ["@hunterjm"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ DUNEHD_PLAYER_SUPPORT: Final[MediaPlayerEntityFeature] = (
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -405,8 +405,13 @@ CT_SENSORS = (
|
||||
)
|
||||
for cttype, key in (
|
||||
(CtType.NET_CONSUMPTION, "lifetime_net_consumption"),
|
||||
# Production CT energy_delivered is not used
|
||||
(CtType.PRODUCTION, "production_ct_energy_delivered"),
|
||||
(CtType.STORAGE, "lifetime_battery_discharged"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_energy_delivered"),
|
||||
(CtType.BACKFEED, "backfeed_ct_energy_delivered"),
|
||||
(CtType.LOAD, "load_ct_energy_delivered"),
|
||||
(CtType.EVSE, "evse_ct_energy_delivered"),
|
||||
(CtType.PV3P, "pv3p_ct_energy_delivered"),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -423,8 +428,13 @@ CT_SENSORS = (
|
||||
)
|
||||
for cttype, key in (
|
||||
(CtType.NET_CONSUMPTION, "lifetime_net_production"),
|
||||
# Production CT energy_received is not used
|
||||
(CtType.PRODUCTION, "production_ct_energy_received"),
|
||||
(CtType.STORAGE, "lifetime_battery_charged"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_energy_received"),
|
||||
(CtType.BACKFEED, "backfeed_ct_energy_received"),
|
||||
(CtType.LOAD, "load_ct_energy_received"),
|
||||
(CtType.EVSE, "evse_ct_energy_received"),
|
||||
(CtType.PV3P, "pv3p_ct_energy_received"),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -441,8 +451,13 @@ CT_SENSORS = (
|
||||
)
|
||||
for cttype, key in (
|
||||
(CtType.NET_CONSUMPTION, "net_consumption"),
|
||||
# Production CT active_power is not used
|
||||
(CtType.PRODUCTION, "production_ct_power"),
|
||||
(CtType.STORAGE, "battery_discharge"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_power"),
|
||||
(CtType.BACKFEED, "backfeed_ct_power"),
|
||||
(CtType.LOAD, "load_ct_power"),
|
||||
(CtType.EVSE, "evse_ct_power"),
|
||||
(CtType.PV3P, "pv3p_ct_power"),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -461,6 +476,11 @@ CT_SENSORS = (
|
||||
(CtType.NET_CONSUMPTION, "frequency", "net_ct_frequency"),
|
||||
(CtType.PRODUCTION, "production_ct_frequency", ""),
|
||||
(CtType.STORAGE, "storage_ct_frequency", ""),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_frequency", ""),
|
||||
(CtType.BACKFEED, "backfeed_ct_frequency", ""),
|
||||
(CtType.LOAD, "load_ct_frequency", ""),
|
||||
(CtType.EVSE, "evse_ct_frequency", ""),
|
||||
(CtType.PV3P, "pv3p_ct_frequency", ""),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -480,6 +500,11 @@ CT_SENSORS = (
|
||||
(CtType.NET_CONSUMPTION, "voltage", "net_ct_voltage"),
|
||||
(CtType.PRODUCTION, "production_ct_voltage", ""),
|
||||
(CtType.STORAGE, "storage_voltage", "storage_ct_voltage"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_voltage", ""),
|
||||
(CtType.BACKFEED, "backfeed_ct_voltage", ""),
|
||||
(CtType.LOAD, "load_ct_voltage", ""),
|
||||
(CtType.EVSE, "evse_ct_voltage", ""),
|
||||
(CtType.PV3P, "pv3p_ct_voltage", ""),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -499,6 +524,11 @@ CT_SENSORS = (
|
||||
(CtType.NET_CONSUMPTION, "net_ct_current"),
|
||||
(CtType.PRODUCTION, "production_ct_current"),
|
||||
(CtType.STORAGE, "storage_ct_current"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_current"),
|
||||
(CtType.BACKFEED, "backfeed_ct_current"),
|
||||
(CtType.LOAD, "load_ct_current"),
|
||||
(CtType.EVSE, "evse_ct_current"),
|
||||
(CtType.PV3P, "pv3p_ct_current"),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -516,6 +546,11 @@ CT_SENSORS = (
|
||||
(CtType.NET_CONSUMPTION, "net_ct_powerfactor"),
|
||||
(CtType.PRODUCTION, "production_ct_powerfactor"),
|
||||
(CtType.STORAGE, "storage_ct_powerfactor"),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_powerfactor"),
|
||||
(CtType.BACKFEED, "backfeed_ct_powerfactor"),
|
||||
(CtType.LOAD, "load_ct_powerfactor"),
|
||||
(CtType.EVSE, "evse_ct_powerfactor"),
|
||||
(CtType.PV3P, "pv3p_ct_powerfactor"),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -537,6 +572,11 @@ CT_SENSORS = (
|
||||
),
|
||||
(CtType.PRODUCTION, "production_ct_metering_status", ""),
|
||||
(CtType.STORAGE, "storage_ct_metering_status", ""),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_metering_status", ""),
|
||||
(CtType.BACKFEED, "backfeed_ct_metering_status", ""),
|
||||
(CtType.LOAD, "load_ct_metering_status", ""),
|
||||
(CtType.EVSE, "evse_ct_metering_status", ""),
|
||||
(CtType.PV3P, "pv3p_ct_metering_status", ""),
|
||||
)
|
||||
]
|
||||
+ [
|
||||
@@ -557,6 +597,11 @@ CT_SENSORS = (
|
||||
),
|
||||
(CtType.PRODUCTION, "production_ct_status_flags", ""),
|
||||
(CtType.STORAGE, "storage_ct_status_flags", ""),
|
||||
(CtType.TOTAL_CONSUMPTION, "total_consumption_ct_status_flags", ""),
|
||||
(CtType.BACKFEED, "backfeed_ct_status_flags", ""),
|
||||
(CtType.LOAD, "load_ct_status_flags", ""),
|
||||
(CtType.EVSE, "evse_ct_status_flags", ""),
|
||||
(CtType.PV3P, "pv3p_ct_status_flags", ""),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -160,6 +160,60 @@
|
||||
"available_energy": {
|
||||
"name": "Available battery energy"
|
||||
},
|
||||
"backfeed_ct_current": {
|
||||
"name": "Backfeed CT current"
|
||||
},
|
||||
"backfeed_ct_current_phase": {
|
||||
"name": "Backfeed CT current {phase_name}"
|
||||
},
|
||||
"backfeed_ct_energy_delivered": {
|
||||
"name": "Backfeed CT energy delivered"
|
||||
},
|
||||
"backfeed_ct_energy_delivered_phase": {
|
||||
"name": "Backfeed CT energy delivered {phase_name}"
|
||||
},
|
||||
"backfeed_ct_energy_received": {
|
||||
"name": "Backfeed CT energy received"
|
||||
},
|
||||
"backfeed_ct_energy_received_phase": {
|
||||
"name": "Backfeed CT energy received {phase_name}"
|
||||
},
|
||||
"backfeed_ct_frequency": {
|
||||
"name": "Frequency backfeed CT"
|
||||
},
|
||||
"backfeed_ct_frequency_phase": {
|
||||
"name": "Frequency backfeed CT {phase_name}"
|
||||
},
|
||||
"backfeed_ct_metering_status": {
|
||||
"name": "Metering status backfeed CT"
|
||||
},
|
||||
"backfeed_ct_metering_status_phase": {
|
||||
"name": "Metering status backfeed CT {phase_name}"
|
||||
},
|
||||
"backfeed_ct_power": {
|
||||
"name": "Backfeed CT power"
|
||||
},
|
||||
"backfeed_ct_power_phase": {
|
||||
"name": "Backfeed CT power {phase_name}"
|
||||
},
|
||||
"backfeed_ct_powerfactor": {
|
||||
"name": "Power factor backfeed CT"
|
||||
},
|
||||
"backfeed_ct_powerfactor_phase": {
|
||||
"name": "Power factor backfeed CT {phase_name}"
|
||||
},
|
||||
"backfeed_ct_status_flags": {
|
||||
"name": "Meter status flags active backfeed CT"
|
||||
},
|
||||
"backfeed_ct_status_flags_phase": {
|
||||
"name": "Meter status flags active backfeed CT {phase_name}"
|
||||
},
|
||||
"backfeed_ct_voltage": {
|
||||
"name": "Voltage backfeed CT"
|
||||
},
|
||||
"backfeed_ct_voltage_phase": {
|
||||
"name": "Voltage backfeed CT {phase_name}"
|
||||
},
|
||||
"balanced_net_consumption": {
|
||||
"name": "Balanced net power consumption"
|
||||
},
|
||||
@@ -211,6 +265,60 @@
|
||||
"energy_today": {
|
||||
"name": "[%key:component::enphase_envoy::entity::sensor::daily_production::name%]"
|
||||
},
|
||||
"evse_ct_current": {
|
||||
"name": "EVSE CT current"
|
||||
},
|
||||
"evse_ct_current_phase": {
|
||||
"name": "EVSE CT current {phase_name}"
|
||||
},
|
||||
"evse_ct_energy_delivered": {
|
||||
"name": "EVSE CT energy delivered"
|
||||
},
|
||||
"evse_ct_energy_delivered_phase": {
|
||||
"name": "EVSE CT energy delivered {phase_name}"
|
||||
},
|
||||
"evse_ct_energy_received": {
|
||||
"name": "EVSE CT energy received"
|
||||
},
|
||||
"evse_ct_energy_received_phase": {
|
||||
"name": "EVSE CT energy received {phase_name}"
|
||||
},
|
||||
"evse_ct_frequency": {
|
||||
"name": "Frequency EVSE CT"
|
||||
},
|
||||
"evse_ct_frequency_phase": {
|
||||
"name": "Frequency EVSE CT {phase_name}"
|
||||
},
|
||||
"evse_ct_metering_status": {
|
||||
"name": "Metering status EVSE CT"
|
||||
},
|
||||
"evse_ct_metering_status_phase": {
|
||||
"name": "Metering status EVSE CT {phase_name}"
|
||||
},
|
||||
"evse_ct_power": {
|
||||
"name": "EVSE CT power"
|
||||
},
|
||||
"evse_ct_power_phase": {
|
||||
"name": "EVSE CT power {phase_name}"
|
||||
},
|
||||
"evse_ct_powerfactor": {
|
||||
"name": "Power factor EVSE CT"
|
||||
},
|
||||
"evse_ct_powerfactor_phase": {
|
||||
"name": "Power factor EVSE CT {phase_name}"
|
||||
},
|
||||
"evse_ct_status_flags": {
|
||||
"name": "Meter status flags active EVSE CT"
|
||||
},
|
||||
"evse_ct_status_flags_phase": {
|
||||
"name": "Meter status flags active EVSE CT {phase_name}"
|
||||
},
|
||||
"evse_ct_voltage": {
|
||||
"name": "Voltage EVSE CT"
|
||||
},
|
||||
"evse_ct_voltage_phase": {
|
||||
"name": "Voltage EVSE CT {phase_name}"
|
||||
},
|
||||
"grid_status": {
|
||||
"name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]",
|
||||
"state": {
|
||||
@@ -270,6 +378,60 @@
|
||||
"lifetime_production_phase": {
|
||||
"name": "Lifetime energy production {phase_name}"
|
||||
},
|
||||
"load_ct_current": {
|
||||
"name": "Load CT current"
|
||||
},
|
||||
"load_ct_current_phase": {
|
||||
"name": "Load CT current {phase_name}"
|
||||
},
|
||||
"load_ct_energy_delivered": {
|
||||
"name": "Load CT energy delivered"
|
||||
},
|
||||
"load_ct_energy_delivered_phase": {
|
||||
"name": "Load CT energy delivered {phase_name}"
|
||||
},
|
||||
"load_ct_energy_received": {
|
||||
"name": "Load CT energy received"
|
||||
},
|
||||
"load_ct_energy_received_phase": {
|
||||
"name": "Load CT energy received {phase_name}"
|
||||
},
|
||||
"load_ct_frequency": {
|
||||
"name": "Frequency load CT"
|
||||
},
|
||||
"load_ct_frequency_phase": {
|
||||
"name": "Frequency load CT {phase_name}"
|
||||
},
|
||||
"load_ct_metering_status": {
|
||||
"name": "Metering status load CT"
|
||||
},
|
||||
"load_ct_metering_status_phase": {
|
||||
"name": "Metering status load CT {phase_name}"
|
||||
},
|
||||
"load_ct_power": {
|
||||
"name": "Load CT power"
|
||||
},
|
||||
"load_ct_power_phase": {
|
||||
"name": "Load CT power {phase_name}"
|
||||
},
|
||||
"load_ct_powerfactor": {
|
||||
"name": "Power factor load CT"
|
||||
},
|
||||
"load_ct_powerfactor_phase": {
|
||||
"name": "Power factor load CT {phase_name}"
|
||||
},
|
||||
"load_ct_status_flags": {
|
||||
"name": "Meter status flags active load CT"
|
||||
},
|
||||
"load_ct_status_flags_phase": {
|
||||
"name": "Meter status flags active load CT {phase_name}"
|
||||
},
|
||||
"load_ct_voltage": {
|
||||
"name": "Voltage load CT"
|
||||
},
|
||||
"load_ct_voltage_phase": {
|
||||
"name": "Voltage load CT {phase_name}"
|
||||
},
|
||||
"max_capacity": {
|
||||
"name": "Battery capacity"
|
||||
},
|
||||
@@ -331,6 +493,18 @@
|
||||
"production_ct_current_phase": {
|
||||
"name": "Production CT current {phase_name}"
|
||||
},
|
||||
"production_ct_energy_delivered": {
|
||||
"name": "Production CT energy delivered"
|
||||
},
|
||||
"production_ct_energy_delivered_phase": {
|
||||
"name": "Production CT energy delivered {phase_name}"
|
||||
},
|
||||
"production_ct_energy_received": {
|
||||
"name": "Production CT energy received"
|
||||
},
|
||||
"production_ct_energy_received_phase": {
|
||||
"name": "Production CT energy received {phase_name}"
|
||||
},
|
||||
"production_ct_frequency": {
|
||||
"name": "Frequency production CT"
|
||||
},
|
||||
@@ -343,6 +517,12 @@
|
||||
"production_ct_metering_status_phase": {
|
||||
"name": "Metering status production CT {phase_name}"
|
||||
},
|
||||
"production_ct_power": {
|
||||
"name": "Production CT power"
|
||||
},
|
||||
"production_ct_power_phase": {
|
||||
"name": "Production CT power {phase_name}"
|
||||
},
|
||||
"production_ct_powerfactor": {
|
||||
"name": "Power factor production CT"
|
||||
},
|
||||
@@ -361,6 +541,60 @@
|
||||
"production_ct_voltage_phase": {
|
||||
"name": "Voltage production CT {phase_name}"
|
||||
},
|
||||
"pv3p_ct_current": {
|
||||
"name": "PV3P CT current"
|
||||
},
|
||||
"pv3p_ct_current_phase": {
|
||||
"name": "PV3P CT current {phase_name}"
|
||||
},
|
||||
"pv3p_ct_energy_delivered": {
|
||||
"name": "PV3P CT energy delivered"
|
||||
},
|
||||
"pv3p_ct_energy_delivered_phase": {
|
||||
"name": "PV3P CT energy delivered {phase_name}"
|
||||
},
|
||||
"pv3p_ct_energy_received": {
|
||||
"name": "PV3P CT energy received"
|
||||
},
|
||||
"pv3p_ct_energy_received_phase": {
|
||||
"name": "PV3P CT energy received {phase_name}"
|
||||
},
|
||||
"pv3p_ct_frequency": {
|
||||
"name": "Frequency PV3P CT"
|
||||
},
|
||||
"pv3p_ct_frequency_phase": {
|
||||
"name": "Frequency PV3P CT {phase_name}"
|
||||
},
|
||||
"pv3p_ct_metering_status": {
|
||||
"name": "Metering status PV3P CT"
|
||||
},
|
||||
"pv3p_ct_metering_status_phase": {
|
||||
"name": "Metering status PV3P CT {phase_name}"
|
||||
},
|
||||
"pv3p_ct_power": {
|
||||
"name": "PV3P CT power"
|
||||
},
|
||||
"pv3p_ct_power_phase": {
|
||||
"name": "PV3P CT power {phase_name}"
|
||||
},
|
||||
"pv3p_ct_powerfactor": {
|
||||
"name": "Power factor PV3P CT"
|
||||
},
|
||||
"pv3p_ct_powerfactor_phase": {
|
||||
"name": "Power factor PV3P CT {phase_name}"
|
||||
},
|
||||
"pv3p_ct_status_flags": {
|
||||
"name": "Meter status flags active PV3P CT"
|
||||
},
|
||||
"pv3p_ct_status_flags_phase": {
|
||||
"name": "Meter status flags active PV3P CT {phase_name}"
|
||||
},
|
||||
"pv3p_ct_voltage": {
|
||||
"name": "Voltage PV3P CT"
|
||||
},
|
||||
"pv3p_ct_voltage_phase": {
|
||||
"name": "Voltage PV3P CT {phase_name}"
|
||||
},
|
||||
"reserve_energy": {
|
||||
"name": "Reserve battery energy"
|
||||
},
|
||||
@@ -414,6 +648,60 @@
|
||||
},
|
||||
"storage_ct_voltage_phase": {
|
||||
"name": "Voltage storage CT {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_current": {
|
||||
"name": "Total consumption CT current"
|
||||
},
|
||||
"total_consumption_ct_current_phase": {
|
||||
"name": "Total consumption CT current {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_energy_delivered": {
|
||||
"name": "Total consumption CT energy delivered"
|
||||
},
|
||||
"total_consumption_ct_energy_delivered_phase": {
|
||||
"name": "Total consumption CT energy delivered {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_energy_received": {
|
||||
"name": "Total consumption CT energy received"
|
||||
},
|
||||
"total_consumption_ct_energy_received_phase": {
|
||||
"name": "Total consumption CT energy received {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_frequency": {
|
||||
"name": "Frequency total consumption CT"
|
||||
},
|
||||
"total_consumption_ct_frequency_phase": {
|
||||
"name": "Frequency total consumption CT {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_metering_status": {
|
||||
"name": "Metering status total consumption CT"
|
||||
},
|
||||
"total_consumption_ct_metering_status_phase": {
|
||||
"name": "Metering status total consumption CT {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_power": {
|
||||
"name": "Total consumption CT power"
|
||||
},
|
||||
"total_consumption_ct_power_phase": {
|
||||
"name": "Total consumption CT power {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_powerfactor": {
|
||||
"name": "Power factor total consumption CT"
|
||||
},
|
||||
"total_consumption_ct_powerfactor_phase": {
|
||||
"name": "Power factor total consumption CT {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_status_flags": {
|
||||
"name": "Meter status flags active total consumption CT"
|
||||
},
|
||||
"total_consumption_ct_status_flags_phase": {
|
||||
"name": "Meter status flags active total consumption CT {phase_name}"
|
||||
},
|
||||
"total_consumption_ct_voltage": {
|
||||
"name": "Voltage total consumption CT"
|
||||
},
|
||||
"total_consumption_ct_voltage_phase": {
|
||||
"name": "Voltage total consumption CT {phase_name}"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -36,12 +36,12 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, ATTR_SETPOINT, EVOHOME_DATA, EvoService
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, DOMAIN, EVOHOME_DATA, EvoService
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
from .entity import EvoChild, EvoEntity
|
||||
|
||||
@@ -132,6 +132,24 @@ class EvoClimateEntity(EvoEntity, ClimateEntity):
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
async def async_clear_zone_override(self) -> None:
|
||||
"""Clear the zone override; only supported by zones."""
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="zone_only_service",
|
||||
translation_placeholders={"service": EvoService.CLEAR_ZONE_OVERRIDE},
|
||||
)
|
||||
|
||||
async def async_set_zone_override(
|
||||
self, setpoint: float, duration: timedelta | None = None
|
||||
) -> None:
|
||||
"""Set the zone override; only supported by zones."""
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="zone_only_service",
|
||||
translation_placeholders={"service": EvoService.SET_ZONE_OVERRIDE},
|
||||
)
|
||||
|
||||
|
||||
class EvoZone(EvoChild, EvoClimateEntity):
|
||||
"""Base for any evohome-compatible heating zone."""
|
||||
@@ -170,22 +188,22 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
"""Process a service request (setpoint override) for a zone."""
|
||||
if service == EvoService.CLEAR_ZONE_OVERRIDE:
|
||||
await self.coordinator.call_client_api(self._evo_device.reset())
|
||||
return
|
||||
async def async_clear_zone_override(self) -> None:
|
||||
"""Clear the zone's override, if any."""
|
||||
await self.coordinator.call_client_api(self._evo_device.reset())
|
||||
|
||||
# otherwise it is EvoService.SET_ZONE_OVERRIDE
|
||||
temperature = max(min(data[ATTR_SETPOINT], self.max_temp), self.min_temp)
|
||||
async def async_set_zone_override(
|
||||
self, setpoint: float, duration: timedelta | None = None
|
||||
) -> None:
|
||||
"""Set the zone's override (mode/setpoint)."""
|
||||
temperature = max(min(setpoint, self.max_temp), self.min_temp)
|
||||
|
||||
if ATTR_DURATION in data:
|
||||
duration: timedelta = data[ATTR_DURATION]
|
||||
if duration is not None:
|
||||
if duration.total_seconds() == 0:
|
||||
await self._update_schedule()
|
||||
until = self.setpoints.get("next_sp_from")
|
||||
else:
|
||||
until = dt_util.now() + data[ATTR_DURATION]
|
||||
until = dt_util.now() + duration
|
||||
else:
|
||||
until = None # indefinitely
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, EvoService
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -47,22 +47,12 @@ class EvoEntity(CoordinatorEntity[EvoDataUpdateCoordinator]):
|
||||
raise NotImplementedError
|
||||
if payload["unique_id"] != self._attr_unique_id:
|
||||
return
|
||||
if payload["service"] in (
|
||||
EvoService.SET_ZONE_OVERRIDE,
|
||||
EvoService.CLEAR_ZONE_OVERRIDE,
|
||||
):
|
||||
await self.async_zone_svc_request(payload["service"], payload["data"])
|
||||
return
|
||||
await self.async_tcs_svc_request(payload["service"], payload["data"])
|
||||
|
||||
async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
"""Process a service request (system mode) for a controller."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
"""Process a service request (setpoint override) for a zone."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||
"""Return the evohome-specific state attributes."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
from typing import Any, Final
|
||||
|
||||
from evohomeasync2.const import SZ_CAN_BE_TEMPORARY, SZ_SYSTEM_MODE, SZ_TIMING_MODE
|
||||
from evohomeasync2.schemas.const import (
|
||||
@@ -13,9 +13,10 @@ from evohomeasync2.schemas.const import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.const import ATTR_MODE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
|
||||
@@ -25,21 +26,38 @@ from .coordinator import EvoDataUpdateCoordinator
|
||||
# system mode schemas are built dynamically when the services are registered
|
||||
# because supported modes can vary for edge-case systems
|
||||
|
||||
CLEAR_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
{vol.Required(ATTR_ENTITY_ID): cv.entity_id}
|
||||
)
|
||||
SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_SETPOINT): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
|
||||
),
|
||||
vol.Optional(ATTR_DURATION): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=0), max=timedelta(days=1)),
|
||||
),
|
||||
}
|
||||
)
|
||||
# Zone service schemas (registered as entity services)
|
||||
CLEAR_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {}
|
||||
SET_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
|
||||
vol.Required(ATTR_SETPOINT): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
|
||||
),
|
||||
vol.Optional(ATTR_DURATION): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=0), max=timedelta(days=1)),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _register_zone_entity_services(hass: HomeAssistant) -> None:
|
||||
"""Register entity-level services for zones."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
EvoService.CLEAR_ZONE_OVERRIDE,
|
||||
entity_domain=CLIMATE_DOMAIN,
|
||||
schema=CLEAR_ZONE_OVERRIDE_SCHEMA,
|
||||
func="async_clear_zone_override",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
EvoService.SET_ZONE_OVERRIDE,
|
||||
entity_domain=CLIMATE_DOMAIN,
|
||||
schema=SET_ZONE_OVERRIDE_SCHEMA,
|
||||
func="async_set_zone_override",
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -51,8 +69,6 @@ def setup_service_functions(
|
||||
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
|
||||
each mode will require any of four distinct service schemas. This has to be
|
||||
enumerated before registering the appropriate handlers.
|
||||
|
||||
It appears that all TCC-compatible systems support the same three zones modes.
|
||||
"""
|
||||
|
||||
@verify_domain_control(DOMAIN)
|
||||
@@ -72,28 +88,6 @@ def setup_service_functions(
|
||||
}
|
||||
async_dispatcher_send(hass, DOMAIN, payload)
|
||||
|
||||
@verify_domain_control(DOMAIN)
|
||||
async def set_zone_override(call: ServiceCall) -> None:
|
||||
"""Set the zone override (setpoint)."""
|
||||
entity_id = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
registry = er.async_get(hass)
|
||||
registry_entry = registry.async_get(entity_id)
|
||||
|
||||
if registry_entry is None or registry_entry.platform != DOMAIN:
|
||||
raise ValueError(f"'{entity_id}' is not a known {DOMAIN} entity")
|
||||
|
||||
if registry_entry.domain != "climate":
|
||||
raise ValueError(f"'{entity_id}' is not an {DOMAIN} controller/zone")
|
||||
|
||||
payload = {
|
||||
"unique_id": registry_entry.unique_id,
|
||||
"service": call.service,
|
||||
"data": call.data,
|
||||
}
|
||||
|
||||
async_dispatcher_send(hass, DOMAIN, payload)
|
||||
|
||||
assert coordinator.tcs is not None # mypy
|
||||
|
||||
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
|
||||
@@ -156,16 +150,4 @@ def setup_service_functions(
|
||||
schema=vol.Schema(vol.Any(*system_mode_schemas)),
|
||||
)
|
||||
|
||||
# The zone modes are consistent across all systems and use the same schema
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
EvoService.CLEAR_ZONE_OVERRIDE,
|
||||
set_zone_override,
|
||||
schema=CLEAR_ZONE_OVERRIDE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
EvoService.SET_ZONE_OVERRIDE,
|
||||
set_zone_override,
|
||||
schema=SET_ZONE_OVERRIDE_SCHEMA,
|
||||
)
|
||||
_register_zone_entity_services(hass)
|
||||
|
||||
@@ -28,14 +28,11 @@ reset_system:
|
||||
refresh_system:
|
||||
|
||||
set_zone_override:
|
||||
target:
|
||||
entity:
|
||||
integration: evohome
|
||||
domain: climate
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
example: climate.bathroom
|
||||
selector:
|
||||
entity:
|
||||
integration: evohome
|
||||
domain: climate
|
||||
setpoint:
|
||||
required: true
|
||||
selector:
|
||||
@@ -49,10 +46,7 @@ set_zone_override:
|
||||
object:
|
||||
|
||||
clear_zone_override:
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: evohome
|
||||
domain: climate
|
||||
target:
|
||||
entity:
|
||||
integration: evohome
|
||||
domain: climate
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"zone_only_service": {
|
||||
"message": "Only zones support the `{service}` action"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"clear_zone_override": {
|
||||
"description": "Sets a zone to follow its schedule.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "[%key:component::evohome::services::set_zone_override::fields::entity_id::description%]",
|
||||
"name": "[%key:component::evohome::services::set_zone_override::fields::entity_id::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Clear zone override"
|
||||
},
|
||||
"refresh_system": {
|
||||
@@ -43,10 +42,6 @@
|
||||
"description": "The zone will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint.",
|
||||
"name": "Duration"
|
||||
},
|
||||
"entity_id": {
|
||||
"description": "The entity ID of the Evohome zone.",
|
||||
"name": "Entity"
|
||||
},
|
||||
"setpoint": {
|
||||
"description": "The temperature to be used instead of the scheduled setpoint.",
|
||||
"name": "Setpoint"
|
||||
|
||||
@@ -45,6 +45,10 @@ async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
|
||||
except BaseException as ex:
|
||||
del stores[user_id]
|
||||
future.set_exception(ex)
|
||||
# Ensure the future is marked as retrieved
|
||||
# since if there is no concurrent call it
|
||||
# will otherwise never be retrieved.
|
||||
future.exception()
|
||||
raise
|
||||
future.set_result(store)
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"audio_unmute": {
|
||||
"default": "mdi:volume-high"
|
||||
},
|
||||
"earc_unmute": {
|
||||
"default": "mdi:volume-high"
|
||||
},
|
||||
"oled_fade": {
|
||||
"default": "mdi:cellphone-information"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,32 @@ class HDFuryNumberEntityDescription(NumberEntityDescription):
|
||||
|
||||
|
||||
NUMBERS: tuple[HDFuryNumberEntityDescription, ...] = (
|
||||
HDFuryNumberEntityDescription(
|
||||
key="unmutecnt",
|
||||
translation_key="audio_unmute",
|
||||
entity_registry_enabled_default=False,
|
||||
mode=NumberMode.BOX,
|
||||
native_min_value=50,
|
||||
native_max_value=1000,
|
||||
native_step=1,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_audio_unmute(value),
|
||||
),
|
||||
HDFuryNumberEntityDescription(
|
||||
key="earcunmutecnt",
|
||||
translation_key="earc_unmute",
|
||||
entity_registry_enabled_default=False,
|
||||
mode=NumberMode.BOX,
|
||||
native_min_value=0,
|
||||
native_max_value=1000,
|
||||
native_step=1,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_earc_unmute(value),
|
||||
),
|
||||
HDFuryNumberEntityDescription(
|
||||
key="oledfade",
|
||||
translation_key="oled_fade",
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"audio_unmute": {
|
||||
"name": "Unmute delay"
|
||||
},
|
||||
"earc_unmute": {
|
||||
"name": "eARC unmute delay"
|
||||
},
|
||||
"oled_fade": {
|
||||
"name": "OLED fade timer"
|
||||
},
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"battery_charge_discharge_state": {
|
||||
"name": "Battery charge/discharge state",
|
||||
"state": {
|
||||
"charging": "Charging",
|
||||
"discharging": "Discharging",
|
||||
"charging": "[%key:common::state::charging%]",
|
||||
"discharging": "[%key:common::state::discharging%]",
|
||||
"static": "Static"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -168,8 +168,9 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
segments: dict[str, Segment] = {}
|
||||
for area in supported_areas:
|
||||
area_name = None
|
||||
if area.areaInfo and area.areaInfo.locationInfo:
|
||||
area_name = area.areaInfo.locationInfo.locationName
|
||||
location_info = area.areaInfo.locationInfo
|
||||
if location_info not in (None, clusters.NullValue):
|
||||
area_name = location_info.locationName
|
||||
|
||||
if area_name:
|
||||
segment_id = str(area.areaID)
|
||||
|
||||
@@ -5,13 +5,13 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import PortainerConfigEntry
|
||||
from .coordinator import PortainerCoordinator
|
||||
|
||||
TO_REDACT = [CONF_API_TOKEN]
|
||||
TO_REDACT = [CONF_API_TOKEN, CONF_URL]
|
||||
|
||||
|
||||
def _serialize_coordinator(coordinator: PortainerCoordinator) -> dict[str, Any]:
|
||||
|
||||
@@ -4,13 +4,19 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from powerfox import DeviceType, Powerfox, PowerfoxConnectionError
|
||||
from powerfox import (
|
||||
DeviceType,
|
||||
Powerfox,
|
||||
PowerfoxAuthenticationError,
|
||||
PowerfoxConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
PowerfoxConfigEntry,
|
||||
PowerfoxDataUpdateCoordinator,
|
||||
@@ -30,9 +36,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) ->
|
||||
|
||||
try:
|
||||
devices = await client.all_devices()
|
||||
except PowerfoxAuthenticationError as err:
|
||||
await client.close()
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_failed",
|
||||
) from err
|
||||
except PowerfoxConnectionError as err:
|
||||
await client.close()
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
) from err
|
||||
|
||||
coordinators: list[
|
||||
PowerfoxDataUpdateCoordinator | PowerfoxReportDataUpdateCoordinator
|
||||
|
||||
@@ -59,18 +59,24 @@ class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
|
||||
except PowerfoxAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": str(err)},
|
||||
translation_key="auth_failed",
|
||||
) from err
|
||||
except (
|
||||
PowerfoxConnectionError,
|
||||
PowerfoxNoDataError,
|
||||
PowerfoxPrivacyError,
|
||||
) as err:
|
||||
except PowerfoxConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
translation_key="connection_error",
|
||||
) from err
|
||||
except PowerfoxNoDataError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_data_error",
|
||||
translation_placeholders={"device_name": self.device.name},
|
||||
) from err
|
||||
except PowerfoxPrivacyError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="privacy_error",
|
||||
translation_placeholders={"device_name": self.device.name},
|
||||
) from err
|
||||
|
||||
async def _async_fetch_data(self) -> T:
|
||||
|
||||
@@ -116,11 +116,17 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_auth": {
|
||||
"message": "Error while authenticating with the Powerfox service: {error}"
|
||||
"auth_failed": {
|
||||
"message": "Authentication with the Powerfox service failed. Please re-authenticate your account."
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Error while updating the Powerfox service: {error}"
|
||||
"connection_error": {
|
||||
"message": "Could not connect to the Powerfox service. Please check your network connection."
|
||||
},
|
||||
"no_data_error": {
|
||||
"message": "No data available for device \"{device_name}\". The device may not have reported data yet."
|
||||
},
|
||||
"privacy_error": {
|
||||
"message": "Data for device \"{device_name}\" is restricted due to privacy settings in the Powerfox app."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,17 @@ class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
if not user_input[CONF_BACKUP_LOCATION].startswith("/"):
|
||||
errors[CONF_BACKUP_LOCATION] = "backup_location_relative"
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
DATA_SCHEMA, user_input
|
||||
),
|
||||
description_placeholders=placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate auth input and save uploaded key file if provided
|
||||
user_input = await self._validate_auth_and_save_keyfile(user_input)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"already_configured": "Integration already configured. Host with same address, port and backup location already exists."
|
||||
},
|
||||
"error": {
|
||||
"backup_location_relative": "The remote path must be an absolute path (starting with `/`).",
|
||||
"invalid_key": "Invalid key uploaded. Please make sure key corresponds to valid SSH key algorithm.",
|
||||
"key_or_password_needed": "Please configure password or private key file location for SFTP Storage.",
|
||||
"os_error": "{error_message}. Please check if host and/or port are correct.",
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"group": "[%key:component::sma::config::step::user::data_description::group%]",
|
||||
"host": "[%key:component::sma::config::step::user::data_description::host%]",
|
||||
"ssl": "[%key:component::sma::config::step::user::data_description::ssl%]",
|
||||
"verify_ssl": "[%key:component::sma::config::step::user::data_description::verify_ssl%]"
|
||||
},
|
||||
"description": "Use the following form to reconfigure your SMA device.",
|
||||
"title": "Reconfigure SMA Solar Integration"
|
||||
},
|
||||
@@ -50,7 +56,11 @@
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your SMA device."
|
||||
"group": "The group of your SMA device, where the Modbus connection is configured",
|
||||
"host": "The hostname or IP address of your SMA device",
|
||||
"password": "The password for your SMA device",
|
||||
"ssl": "Whether to use SSL to connect to your SMA device. This is required for newer SMA devices, but older devices do not support SSL",
|
||||
"verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate"
|
||||
},
|
||||
"description": "Enter your SMA device information.",
|
||||
"title": "Set up SMA Solar"
|
||||
|
||||
@@ -177,6 +177,12 @@
|
||||
"on": "mdi:lightbulb-on"
|
||||
}
|
||||
},
|
||||
"do_not_disturb": {
|
||||
"default": "mdi:minus-circle-off",
|
||||
"state": {
|
||||
"on": "mdi:minus-circle"
|
||||
}
|
||||
},
|
||||
"dry_plus": {
|
||||
"default": "mdi:heat-wave"
|
||||
},
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.5.3"]
|
||||
"requirements": ["pysmartthings==3.6.0"]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ ROBOT_CLEANER_TURBO_MODE_STATE_MAP = {
|
||||
|
||||
ROBOT_CLEANER_MOVEMENT_MAP = {
|
||||
"powerOff": "off",
|
||||
"washingMop": "washing_mop",
|
||||
}
|
||||
|
||||
OVEN_MODE = {
|
||||
@@ -880,6 +881,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
"after",
|
||||
"cleaning",
|
||||
"pause",
|
||||
"washing_mop",
|
||||
],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda value: ROBOT_CLEANER_MOVEMENT_MAP.get(value, value),
|
||||
|
||||
@@ -718,7 +718,8 @@
|
||||
"off": "[%key:common::state::off%]",
|
||||
"pause": "[%key:common::state::paused%]",
|
||||
"point": "Point",
|
||||
"reserve": "Reserve"
|
||||
"reserve": "Reserve",
|
||||
"washing_mop": "Washing mop"
|
||||
}
|
||||
},
|
||||
"robot_cleaner_turbo_mode": {
|
||||
@@ -858,6 +859,9 @@
|
||||
"display_lighting": {
|
||||
"name": "Display lighting"
|
||||
},
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"dry_plus": {
|
||||
"name": "Dry plus"
|
||||
},
|
||||
|
||||
@@ -162,6 +162,14 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
|
||||
status_attribute=Attribute.STATUS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.CUSTOM_DO_NOT_DISTURB_MODE: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.CUSTOM_DO_NOT_DISTURB_MODE,
|
||||
translation_key="do_not_disturb",
|
||||
status_attribute=Attribute.DO_NOT_DISTURB,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
on_command=Command.DO_NOT_DISTURB_ON,
|
||||
off_command=Command.DO_NOT_DISTURB_OFF,
|
||||
),
|
||||
}
|
||||
DISHWASHER_WASHING_OPTIONS_TO_SWITCHES: dict[
|
||||
Attribute | str, SmartThingsDishwasherWashingOptionSwitchEntityDescription
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==1.0.0"]
|
||||
"requirements": ["PySwitchbot==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -257,6 +257,9 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
) -> StateType | date | datetime | Decimal | None:
|
||||
"""Validate the state."""
|
||||
if self._numeric_state_expected:
|
||||
if not isinstance(result, bool) and isinstance(result, (int, float)):
|
||||
return result
|
||||
|
||||
return template_validators.number(self, CONF_STATE)(result)
|
||||
|
||||
if result is None or self.device_class not in (
|
||||
|
||||
@@ -236,12 +236,6 @@ class StateVacuumEntity(
|
||||
if self.__vacuum_legacy_battery_icon:
|
||||
self._report_deprecated_battery_properties("battery_icon")
|
||||
|
||||
@callback
|
||||
def async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine."""
|
||||
super().async_write_ha_state()
|
||||
self._async_check_segments_issues()
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self) -> None:
|
||||
"""Run when the entity registry entry has been updated."""
|
||||
@@ -514,43 +508,6 @@ class StateVacuumEntity(
|
||||
return
|
||||
|
||||
options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {})
|
||||
should_have_not_configured_issue = (
|
||||
VacuumEntityFeature.CLEAN_AREA in self.supported_features
|
||||
and options.get("area_mapping") is None
|
||||
)
|
||||
|
||||
if (
|
||||
should_have_not_configured_issue
|
||||
and not self._segments_not_configured_issue_created
|
||||
):
|
||||
issue_id = (
|
||||
f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}"
|
||||
)
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
data={
|
||||
"entry_id": self.registry_entry.id,
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED,
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
self._segments_not_configured_issue_created = True
|
||||
elif (
|
||||
not should_have_not_configured_issue
|
||||
and self._segments_not_configured_issue_created
|
||||
):
|
||||
issue_id = (
|
||||
f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}"
|
||||
)
|
||||
ir.async_delete_issue(self.hass, DOMAIN, issue_id)
|
||||
self._segments_not_configured_issue_created = False
|
||||
|
||||
if self._segments_changed_last_seen is not None and (
|
||||
VacuumEntityFeature.CLEAN_AREA not in self.supported_features
|
||||
|
||||
@@ -93,10 +93,6 @@
|
||||
"segments_changed": {
|
||||
"description": "",
|
||||
"title": "Vacuum segments have changed for {entity_id}"
|
||||
},
|
||||
"segments_mapping_not_configured": {
|
||||
"description": "",
|
||||
"title": "Vacuum segment mapping not configured for {entity_id}"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "wled",
|
||||
"name": "WLED",
|
||||
"codeowners": ["@frenck"],
|
||||
"codeowners": ["@frenck", "@mik-laj"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wled",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -274,6 +274,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload ZHA config entry."""
|
||||
if not await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS):
|
||||
return False
|
||||
|
||||
ha_zha_data = get_zha_data(hass)
|
||||
ha_zha_data.config_entry = None
|
||||
|
||||
@@ -281,6 +284,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
await ha_zha_data.gateway_proxy.shutdown()
|
||||
ha_zha_data.gateway_proxy = None
|
||||
|
||||
ha_zha_data.update_coordinator = None
|
||||
|
||||
# clean up any remaining entity metadata
|
||||
# (entities that have been discovered but not yet added to HA)
|
||||
# suppress KeyError because we don't know what state we may
|
||||
@@ -291,7 +296,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
|
||||
websocket_api.async_unload_api(hass)
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -161,7 +161,6 @@ FLOWS = {
|
||||
"dsmr",
|
||||
"dsmr_reader",
|
||||
"duckdns",
|
||||
"duke_energy",
|
||||
"dunehd",
|
||||
"duotecno",
|
||||
"dwd_weather_warnings",
|
||||
|
||||
@@ -1497,12 +1497,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"duke_energy": {
|
||||
"name": "Duke Energy",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"dunehd": {
|
||||
"name": "Dune HD",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -32,8 +32,11 @@ def write_utf8_file_atomic(
|
||||
Using this function frequently will significantly
|
||||
negatively impact performance.
|
||||
"""
|
||||
encoding = "utf-8" if "b" not in mode else None
|
||||
try:
|
||||
with AtomicWriter(filename, mode=mode, overwrite=True).open() as fdesc:
|
||||
with AtomicWriter( # type: ignore[call-arg] # atomicwrites-stubs is outdated, encoding is a valid kwarg
|
||||
filename, mode=mode, overwrite=True, encoding=encoding
|
||||
).open() as fdesc:
|
||||
if not private:
|
||||
os.fchmod(fdesc.fileno(), 0o644)
|
||||
fdesc.write(utf8_data)
|
||||
|
||||
7
requirements_all.txt
generated
7
requirements_all.txt
generated
@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
|
||||
PySrDaliGateway==0.19.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==1.0.0
|
||||
PySwitchbot==1.1.0
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@@ -235,9 +235,6 @@ aiodiscover==2.7.1
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.0
|
||||
|
||||
# homeassistant.components.duke_energy
|
||||
aiodukeenergy==0.3.0
|
||||
|
||||
# homeassistant.components.eafm
|
||||
aioeafm==0.1.2
|
||||
|
||||
@@ -2476,7 +2473,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.5.3
|
||||
pysmartthings==3.6.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
|
||||
7
requirements_test_all.txt
generated
7
requirements_test_all.txt
generated
@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
|
||||
PySrDaliGateway==0.19.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==1.0.0
|
||||
PySwitchbot==1.1.0
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
PySyncThru==0.8.0
|
||||
@@ -226,9 +226,6 @@ aiodiscover==2.7.1
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.0
|
||||
|
||||
# homeassistant.components.duke_energy
|
||||
aiodukeenergy==0.3.0
|
||||
|
||||
# homeassistant.components.eafm
|
||||
aioeafm==0.1.2
|
||||
|
||||
@@ -2108,7 +2105,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.5.3
|
||||
pysmartthings==3.6.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
|
||||
@@ -295,7 +295,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"dsmr",
|
||||
"dsmr_reader",
|
||||
"dublin_bus_transport",
|
||||
"duke_energy",
|
||||
"dunehd",
|
||||
"duotecno",
|
||||
"dwd_weather_warnings",
|
||||
@@ -1273,7 +1272,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"dsmr",
|
||||
"dsmr_reader",
|
||||
"dublin_bus_transport",
|
||||
"duke_energy",
|
||||
"dunehd",
|
||||
"duotecno",
|
||||
"dwd_weather_warnings",
|
||||
|
||||
@@ -1,23 +1 @@
|
||||
"""Tests for the AdGuard Home integration."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
adguard_mock: AsyncGenerator,
|
||||
) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"homeassistant.components.adguard.AdGuardHome",
|
||||
return_value=adguard_mock,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Common fixtures for the adguard tests."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from adguardhome import AdGuardHome
|
||||
from adguardhome.filtering import AdGuardHomeFiltering
|
||||
@@ -12,7 +13,7 @@ from adguardhome.stats import AdGuardHomeStats
|
||||
from adguardhome.update import AdGuardHomeAvailableUpdate, AdGuardHomeUpdate
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.adguard import DOMAIN
|
||||
from homeassistant.components.adguard import DOMAIN, PLATFORMS
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -20,7 +21,9 @@ from homeassistant.const import (
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -43,8 +46,8 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_adguard() -> AsyncMock:
|
||||
"""Fixture for setting up the component."""
|
||||
def mock_adguard() -> Generator[AsyncMock]:
|
||||
"""Return a mocked AdGuard Home client."""
|
||||
adguard_mock = AsyncMock(spec=AdGuardHome)
|
||||
adguard_mock.filtering = AsyncMock(spec=AdGuardHomeFiltering)
|
||||
adguard_mock.parental = AsyncMock(spec=AdGuardHomeParental)
|
||||
@@ -86,4 +89,31 @@ async def mock_adguard() -> AsyncMock:
|
||||
)
|
||||
)
|
||||
|
||||
return adguard_mock
|
||||
with patch(
|
||||
"homeassistant.components.adguard.AdGuardHome",
|
||||
return_value=adguard_mock,
|
||||
):
|
||||
yield adguard_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return PLATFORMS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_adguard: AsyncMock,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the AdGuard Home integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
"""Tests for the AdGuard Home."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from adguardhome import AdGuardHomeConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard setup."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", []):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@@ -31,5 +34,8 @@ async def test_setup_failed(
|
||||
"""Test the adguard setup failed."""
|
||||
mock_adguard.version.side_effect = AdGuardHomeConnectionError("Connection error")
|
||||
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for the AdGuard Home sensor entities."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -9,21 +7,21 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard sensor platform."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,22 +14,22 @@ from homeassistant.components.adguard.const import (
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_REMOVE_URL,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return []
|
||||
|
||||
|
||||
async def test_service_registration(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard services be registered."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", []):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
services = hass.services.async_services_for_domain(DOMAIN)
|
||||
|
||||
assert len(services) == 5
|
||||
@@ -73,15 +73,11 @@ async def test_service_registration(
|
||||
async def test_service(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
service_call_data: dict,
|
||||
call_assertion: Callable[[AsyncMock], Any],
|
||||
) -> None:
|
||||
"""Test the adguard services be unregistered with unloading last entry."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", []):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from adguardhome import AdGuardHomeError
|
||||
import pytest
|
||||
@@ -14,23 +14,26 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SWITCH]
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_switch(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard switch platform."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@@ -103,15 +106,11 @@ async def test_switch(
|
||||
async def test_switch_actions(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
switch_name: str,
|
||||
service: str,
|
||||
call_assertion: Callable[[AsyncMock], Any],
|
||||
) -> None:
|
||||
"""Test the adguard switch actions."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
service,
|
||||
@@ -138,7 +137,6 @@ async def test_switch_actions(
|
||||
async def test_switch_action_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
service: str,
|
||||
expected_message: str,
|
||||
@@ -146,9 +144,6 @@ async def test_switch_action_failed(
|
||||
"""Test the adguard switch actions."""
|
||||
caplog.set_level(logging.ERROR)
|
||||
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
mock_adguard.enable_protection.side_effect = AdGuardHomeError("Boom")
|
||||
mock_adguard.disable_protection.side_effect = AdGuardHomeError("Boom")
|
||||
|
||||
|
||||
@@ -12,22 +12,23 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.UPDATE]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_update(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard update platform."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@@ -41,41 +42,38 @@ async def test_update_disabled(
|
||||
disabled=True,
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.states.async_all()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_update_install(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard update installation."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await hass.services.async_call(
|
||||
"update",
|
||||
"install",
|
||||
{"entity_id": "update.adguard_home"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_adguard.update.begin_update.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_update_install_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard update install failed."""
|
||||
mock_adguard.update.begin_update.side_effect = AdGuardHomeError("boom")
|
||||
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
"update",
|
||||
|
||||
@@ -213,6 +213,7 @@ async def test_reauth_flow_scenario(
|
||||
ap_fixture: AirOSData,
|
||||
mock_airos_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful reauthentication."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@@ -220,11 +221,15 @@ async def test_reauth_flow_scenario(
|
||||
mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
flow = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_REAUTH, "entry_id": mock_config_entry.entry_id},
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
||||
side_effect=AirOSConnectionAuthenticationError,
|
||||
):
|
||||
flow = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_REAUTH, "entry_id": mock_config_entry.entry_id},
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
assert flow["type"] == FlowResultType.FORM
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
@@ -236,20 +241,22 @@ async def test_reauth_flow_scenario(
|
||||
hostname=ap_fixture.host.hostname,
|
||||
)
|
||||
|
||||
mock_firmware = AsyncMock(return_value=valid_data)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
||||
new=AsyncMock(return_value=valid_data),
|
||||
new=mock_firmware,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.async_get_firmware_data",
|
||||
new=AsyncMock(return_value=valid_data),
|
||||
new=mock_firmware,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow["flow_id"],
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Always test resolution
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
||||
@@ -70,6 +70,15 @@ def dummy_client_fixture() -> Generator[MagicMock]:
|
||||
yield client.return_value
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.arcam_fmj.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
async def test_ssdp(hass: HomeAssistant) -> None:
|
||||
"""Test a ssdp import flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
@@ -19,18 +19,16 @@ async def test_setup_entry(config_entry_setup: MockConfigEntry) -> None:
|
||||
async def test_setup_entry_fails(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test successful setup of entry."""
|
||||
"""Test failed setup of entry."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
mock_device = Mock()
|
||||
mock_device.async_setup = AsyncMock(return_value=False)
|
||||
with patch(
|
||||
"homeassistant.components.axis.get_axis_api",
|
||||
side_effect=axis.CannotConnect,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
with patch.object(axis, "AxisHub") as mock_device_class:
|
||||
mock_device_class.return_value = mock_device
|
||||
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.bring.const import DOMAIN
|
||||
from homeassistant.components.bring.services import (
|
||||
ATTR_ITEM_NAME,
|
||||
ATTR_NOTIFICATION_TYPE,
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
@@ -33,7 +34,7 @@ async def test_send_notification(
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_message",
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
service_data={
|
||||
ATTR_NOTIFICATION_TYPE: "GOING_SHOPPING",
|
||||
},
|
||||
@@ -64,7 +65,7 @@ async def test_send_notification_exception(
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_message",
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
service_data={
|
||||
ATTR_NOTIFICATION_TYPE: "GOING_SHOPPING",
|
||||
},
|
||||
@@ -90,7 +91,7 @@ async def test_send_notification_service_validation_error(
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_message",
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
service_data={ATTR_NOTIFICATION_TYPE: "URGENT_MESSAGE", ATTR_ITEM_NAME: ""},
|
||||
target={ATTR_ENTITY_ID: "todo.einkauf"},
|
||||
blocking=True,
|
||||
|
||||
@@ -12,7 +12,10 @@ from bring_api import (
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bring.const import DOMAIN
|
||||
from homeassistant.components.bring.services import ATTR_REACTION
|
||||
from homeassistant.components.bring.services import (
|
||||
ATTR_REACTION,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -48,7 +51,7 @@ async def test_send_reaction(
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: reaction,
|
||||
@@ -82,7 +85,7 @@ async def test_send_reaction_exception(
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
@@ -110,7 +113,7 @@ async def test_send_reaction_config_entry_not_loaded(
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
@@ -141,7 +144,7 @@ async def test_send_reaction_unknown_entity(
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
@@ -174,7 +177,7 @@ async def test_send_reaction_not_found(
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"send_reaction",
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
'object_id_base': 'Total energy',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
@@ -18,6 +19,7 @@ ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature"
|
||||
ENTITY_TOTAL_ENERGY = "sensor.bsb_lan_total_energy"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_entity_properties(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
|
||||
@@ -10,10 +10,6 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bsblan.const import DOMAIN
|
||||
from homeassistant.components.bsblan.services import (
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
async_setup_services,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -134,7 +130,7 @@ async def test_set_hot_water_schedule(
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
service_call_data,
|
||||
blocking=True,
|
||||
)
|
||||
@@ -163,7 +159,7 @@ async def test_invalid_device_id(
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": "invalid_device_id",
|
||||
"monday_slots": [
|
||||
@@ -176,11 +172,12 @@ async def test_invalid_device_id(
|
||||
assert exc_info.value.translation_key == "invalid_device_id"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("service_name", "service_data"),
|
||||
[
|
||||
(
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{"monday_slots": [{"start_time": time(6, 0), "end_time": time(8, 0)}]},
|
||||
),
|
||||
("sync_time", {}),
|
||||
@@ -205,9 +202,6 @@ async def test_no_config_entry_for_device(
|
||||
name="Other Device",
|
||||
)
|
||||
|
||||
# Register the bsblan service without setting up any bsblan config entry
|
||||
async_setup_services(hass)
|
||||
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -222,26 +216,15 @@ async def test_no_config_entry_for_device(
|
||||
async def test_config_entry_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
device_entry: dr.DeviceEntry,
|
||||
) -> None:
|
||||
"""Test error when config entry is not loaded."""
|
||||
# Add the config entry but don't set it up (so it stays in NOT_LOADED state)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Create the device manually since setup won't run
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
identifiers={(DOMAIN, TEST_DEVICE_MAC)},
|
||||
name="BSB-LAN Device",
|
||||
)
|
||||
|
||||
# Register the service
|
||||
async_setup_services(hass)
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -266,7 +249,7 @@ async def test_api_error(
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -302,7 +285,7 @@ async def test_time_validation_errors(
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -325,7 +308,7 @@ async def test_unprovided_days_are_none(
|
||||
# Only provide Monday and Tuesday, leave other days unprovided
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -369,7 +352,7 @@ async def test_string_time_formats(
|
||||
# Test with string time formats
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -406,7 +389,7 @@ async def test_non_standard_time_types(
|
||||
with pytest.raises(vol.MultipleInvalid):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
"set_hot_water_schedule",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"monday_slots": [
|
||||
@@ -424,7 +407,7 @@ async def test_async_setup_services(
|
||||
) -> None:
|
||||
"""Test service registration."""
|
||||
# Verify service doesn't exist initially
|
||||
assert not hass.services.has_service(DOMAIN, SERVICE_SET_HOT_WATER_SCHEDULE)
|
||||
assert not hass.services.has_service(DOMAIN, "set_hot_water_schedule")
|
||||
|
||||
# Set up the integration
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@@ -432,7 +415,7 @@ async def test_async_setup_services(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify service is now registered
|
||||
assert hass.services.has_service(DOMAIN, SERVICE_SET_HOT_WATER_SCHEDULE)
|
||||
assert hass.services.has_service(DOMAIN, "set_hot_water_schedule")
|
||||
|
||||
|
||||
async def test_sync_time_service(
|
||||
|
||||
@@ -2385,3 +2385,42 @@ async def test_ha_cast(hass: HomeAssistant, ha_controller_mock) -> None:
|
||||
chromecast.unregister_handler.reset_mock()
|
||||
unregister_cb()
|
||||
chromecast.unregister_handler.assert_not_called()
|
||||
|
||||
|
||||
async def test_entity_media_states_active_app_reported_idle(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test entity state when app is active but device reports idle (fixes #160814)."""
|
||||
entity_id = "media_player.speaker"
|
||||
info = get_fake_chromecast_info()
|
||||
chromecast, _ = await async_setup_media_player_cast(hass, info)
|
||||
cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast)
|
||||
|
||||
# Connect the device
|
||||
connection_status = MagicMock()
|
||||
connection_status.status = "CONNECTED"
|
||||
conn_status_cb(connection_status)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Scenario: Custom App is running (e.g. DashCast), but device reports is_idle=True
|
||||
chromecast.app_id = "84912283" # Example Custom App ID
|
||||
chromecast.is_idle = True # Device thinks it's idle/standby
|
||||
|
||||
# Trigger a status update
|
||||
cast_status = MagicMock()
|
||||
cast_status_cb(cast_status)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "idle"
|
||||
|
||||
# Scenario: Backdrop (Screensaver) is running. Should still be OFF.
|
||||
chromecast.app_id = pychromecast.config.APP_BACKDROP
|
||||
chromecast.is_idle = True
|
||||
|
||||
cast_status_cb(cast_status)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
||||
@@ -79,7 +79,9 @@ async def test_form_invalid_host(
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -111,7 +113,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_form_unexpected_error(hass: HomeAssistant) -> None:
|
||||
async def test_form_unexpected_error(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Tests for the Daikin config flow."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import PropertyMock, patch
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from aiohttp import ClientError, web_exceptions
|
||||
from pydaikin.exceptions import DaikinException
|
||||
@@ -20,6 +21,15 @@ MAC = "AABBCCDDEEFF"
|
||||
HOST = "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.daikin.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_daikin():
|
||||
"""Mock pydaikin."""
|
||||
|
||||
@@ -30,7 +30,7 @@ def recorder_url_mock():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_bluetooth", "mock_zeroconf")
|
||||
@pytest.mark.usefixtures("mock_bluetooth", "mock_zeroconf", "socket_enabled")
|
||||
async def test_setup(hass: HomeAssistant) -> None:
|
||||
"""Test setup."""
|
||||
recorder_helper.async_initialize_recorder(hass)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for the Duke Energy integration."""
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Common fixtures for the Duke Energy tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.duke_energy.const import DOMAIN
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import RecorderInstanceContextManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_recorder_before_hass(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
) -> None:
|
||||
"""Set up recorder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.duke_energy.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(hass: HomeAssistant) -> Generator[AsyncMock]:
|
||||
"""Return the default mocked config entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_EMAIL: "test@example.com",
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api() -> Generator[AsyncMock]:
|
||||
"""Mock a successful Duke Energy API."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.duke_energy.config_flow.DukeEnergy",
|
||||
autospec=True,
|
||||
) as mock_api,
|
||||
patch(
|
||||
"homeassistant.components.duke_energy.coordinator.DukeEnergy",
|
||||
new=mock_api,
|
||||
),
|
||||
):
|
||||
api = mock_api.return_value
|
||||
api.authenticate.return_value = {
|
||||
"loginEmailAddress": "TEST@EXAMPLE.COM",
|
||||
"internalUserID": "test-username",
|
||||
}
|
||||
api.get_meters.return_value = {}
|
||||
yield api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_with_meters(mock_api: AsyncMock) -> AsyncMock:
|
||||
"""Mock a successful Duke Energy API with meters."""
|
||||
mock_api.get_meters.return_value = {
|
||||
"123": {
|
||||
"serialNum": "123",
|
||||
"serviceType": "ELECTRIC",
|
||||
"agreementActiveDate": "2000-01-01",
|
||||
},
|
||||
}
|
||||
mock_api.get_energy_usage.return_value = {
|
||||
"data": {
|
||||
dt_util.now(): {
|
||||
"energy": 1.3,
|
||||
"temperature": 70,
|
||||
}
|
||||
},
|
||||
"missing": [],
|
||||
}
|
||||
return mock_api
|
||||
@@ -1,118 +0,0 @@
|
||||
"""Test the Duke Energy config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.duke_energy.const import DOMAIN
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
|
||||
async def test_user(
|
||||
hass: HomeAssistant,
|
||||
recorder_mock: Recorder,
|
||||
mock_api: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user config."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
|
||||
# test with all provided
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "test@example.com"
|
||||
|
||||
data = result.get("data")
|
||||
assert data
|
||||
assert data[CONF_USERNAME] == "test-username"
|
||||
assert data[CONF_PASSWORD] == "test-password"
|
||||
assert data[CONF_EMAIL] == "test@example.com"
|
||||
|
||||
|
||||
async def test_abort_if_already_setup(
|
||||
hass: HomeAssistant,
|
||||
recorder_mock: Recorder,
|
||||
mock_api: AsyncMock,
|
||||
mock_config_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we abort if the email is already setup."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_abort_if_already_setup_alternate_username(
|
||||
hass: HomeAssistant,
|
||||
recorder_mock: Recorder,
|
||||
mock_api: AsyncMock,
|
||||
mock_config_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we abort if the email is already setup."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(ClientResponseError(None, None, status=404), "invalid_auth"),
|
||||
(ClientResponseError(None, None, status=500), "cannot_connect"),
|
||||
(TimeoutError(), "cannot_connect"),
|
||||
(ClientError(), "cannot_connect"),
|
||||
(Exception(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_api_errors(
|
||||
hass: HomeAssistant,
|
||||
recorder_mock: Recorder,
|
||||
mock_api: Mock,
|
||||
side_effect,
|
||||
expected_error,
|
||||
) -> None:
|
||||
"""Test the failure scenarios."""
|
||||
mock_api.authenticate.side_effect = side_effect
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("errors") == {"base": expected_error}
|
||||
|
||||
mock_api.authenticate.side_effect = None
|
||||
|
||||
# test with all provided
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Tests for the SolarEdge coordinator services."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_update(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_api_with_meters: Mock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
recorder_mock: Recorder,
|
||||
) -> None:
|
||||
"""Test Coordinator."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_api_with_meters.get_meters.call_count == 1
|
||||
# 3 years of data
|
||||
assert mock_api_with_meters.get_energy_usage.call_count == 37
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.duke_energy.coordinator.get_last_statistics",
|
||||
return_value={
|
||||
"duke_energy:electric_123_energy_consumption": [
|
||||
{"start": dt_util.now().timestamp()}
|
||||
]
|
||||
},
|
||||
):
|
||||
freezer.tick(timedelta(hours=12))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert mock_api_with_meters.get_meters.call_count == 2
|
||||
# Now have stats, so only one call
|
||||
assert mock_api_with_meters.get_energy_usage.call_count == 38
|
||||
@@ -849,15 +849,19 @@ async def test_unknown_exception(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=True, sync_complete=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.Elk",
|
||||
return_value=mocked_elk,
|
||||
with (
|
||||
_patch_discovery(no_device=True),
|
||||
patch(
|
||||
"homeassistant.components.elkm1.config_flow.Elk",
|
||||
return_value=mocked_elk,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -914,15 +918,19 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_form_invalid_auth_no_password(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth error when no password is provided."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=True, sync_complete=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.Elk",
|
||||
return_value=mocked_elk,
|
||||
with (
|
||||
_patch_discovery(no_device=True),
|
||||
patch(
|
||||
"homeassistant.components.elkm1.config_flow.Elk",
|
||||
return_value=mocked_elk,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1991,6 +1999,7 @@ async def test_reconfigure_nonsecure(
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with (
|
||||
_patch_discovery(no_device=True),
|
||||
_patch_elk(mocked_elk),
|
||||
patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Tests for emulated_roku config flow."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.emulated_roku import config_flow
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -8,6 +13,15 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.emulated_roku.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
async def test_flow_works(hass: HomeAssistant) -> None:
|
||||
"""Test that config flow works."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
@@ -196,6 +196,66 @@
|
||||
"measurement_type": "storage",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"backfeed": {
|
||||
"eid": "100000040",
|
||||
"timestamp": 1708006120,
|
||||
"energy_delivered": 41234,
|
||||
"energy_received": 42345,
|
||||
"active_power": 104,
|
||||
"power_factor": 0.24,
|
||||
"voltage": 114,
|
||||
"current": 0.5,
|
||||
"frequency": 50.4,
|
||||
"state": "enabled",
|
||||
"measurement_type": "backfeed",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"load": {
|
||||
"eid": "100000050",
|
||||
"timestamp": 1708006120,
|
||||
"energy_delivered": 51234,
|
||||
"energy_received": 52345,
|
||||
"active_power": 105,
|
||||
"power_factor": 0.25,
|
||||
"voltage": 115,
|
||||
"current": 0.6,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "load",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"evse": {
|
||||
"eid": "100000060",
|
||||
"timestamp": 1708006120,
|
||||
"energy_delivered": 61234,
|
||||
"energy_received": 62345,
|
||||
"active_power": 106,
|
||||
"power_factor": 0.26,
|
||||
"voltage": 116,
|
||||
"current": 0.7,
|
||||
"frequency": 50.7,
|
||||
"state": "enabled",
|
||||
"measurement_type": "evse",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"pv3p": {
|
||||
"eid": "100000070",
|
||||
"timestamp": 1708006120,
|
||||
"energy_delivered": 71234,
|
||||
"energy_received": 72345,
|
||||
"active_power": 107,
|
||||
"power_factor": 0.27,
|
||||
"voltage": 117,
|
||||
"current": 0.8,
|
||||
"frequency": 50.8,
|
||||
"state": "enabled",
|
||||
"measurement_type": "pv3p",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
},
|
||||
"ctmeters_phases": {
|
||||
@@ -339,6 +399,194 @@
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
},
|
||||
"backfeed": {
|
||||
"L1": {
|
||||
"eid": "100000041",
|
||||
"timestamp": 1708006121,
|
||||
"energy_delivered": 412341,
|
||||
"energy_received": 423451,
|
||||
"active_power": 114,
|
||||
"power_factor": 0.24,
|
||||
"voltage": 114,
|
||||
"current": 4.1,
|
||||
"frequency": 50.4,
|
||||
"state": "enabled",
|
||||
"measurement_type": "backfeed",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L2": {
|
||||
"eid": "100000042",
|
||||
"timestamp": 1708006122,
|
||||
"energy_delivered": 412342,
|
||||
"energy_received": 423452,
|
||||
"active_power": 124,
|
||||
"power_factor": 0.24,
|
||||
"voltage": 114,
|
||||
"current": 4.2,
|
||||
"frequency": 50.4,
|
||||
"state": "enabled",
|
||||
"measurement_type": "backfeed",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L3": {
|
||||
"eid": "100000042",
|
||||
"timestamp": 1708006123,
|
||||
"energy_delivered": 412343,
|
||||
"energy_received": 423453,
|
||||
"active_power": 134,
|
||||
"power_factor": 0.24,
|
||||
"voltage": 114,
|
||||
"current": 4.3,
|
||||
"frequency": 50.4,
|
||||
"state": "enabled",
|
||||
"measurement_type": "backfeed",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
},
|
||||
"load": {
|
||||
"L1": {
|
||||
"eid": "100000051",
|
||||
"timestamp": 1708006121,
|
||||
"energy_delivered": 512341,
|
||||
"energy_received": 523451,
|
||||
"active_power": 115,
|
||||
"power_factor": 0.25,
|
||||
"voltage": 115,
|
||||
"current": 5.1,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "load",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L2": {
|
||||
"eid": "100000052",
|
||||
"timestamp": 1708006122,
|
||||
"energy_delivered": 512342,
|
||||
"energy_received": 523452,
|
||||
"active_power": 125,
|
||||
"power_factor": 0.25,
|
||||
"voltage": 115,
|
||||
"current": 5.2,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "load",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L3": {
|
||||
"eid": "100000052",
|
||||
"timestamp": 1708006123,
|
||||
"energy_delivered": 512343,
|
||||
"energy_received": 523453,
|
||||
"active_power": 135,
|
||||
"power_factor": 0.25,
|
||||
"voltage": 115,
|
||||
"current": 5.3,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "load",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
},
|
||||
"evse": {
|
||||
"L1": {
|
||||
"eid": "100000061",
|
||||
"timestamp": 1708006121,
|
||||
"energy_delivered": 612341,
|
||||
"energy_received": 623451,
|
||||
"active_power": 116,
|
||||
"power_factor": 0.26,
|
||||
"voltage": 116,
|
||||
"current": 6.1,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "evse",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L2": {
|
||||
"eid": "100000062",
|
||||
"timestamp": 1708006122,
|
||||
"energy_delivered": 612342,
|
||||
"energy_received": 623452,
|
||||
"active_power": 126,
|
||||
"power_factor": 0.26,
|
||||
"voltage": 116,
|
||||
"current": 6.2,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "evse",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L3": {
|
||||
"eid": "100000063",
|
||||
"timestamp": 1708006123,
|
||||
"energy_delivered": 612343,
|
||||
"energy_received": 623453,
|
||||
"active_power": 136,
|
||||
"power_factor": 0.26,
|
||||
"voltage": 116,
|
||||
"current": 6.3,
|
||||
"frequency": 50.6,
|
||||
"state": "enabled",
|
||||
"measurement_type": "evse",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
},
|
||||
"pv3p": {
|
||||
"L1": {
|
||||
"eid": "100000071",
|
||||
"timestamp": 1708006127,
|
||||
"energy_delivered": 712341,
|
||||
"energy_received": 723451,
|
||||
"active_power": 117,
|
||||
"power_factor": 0.27,
|
||||
"voltage": 117,
|
||||
"current": 7.1,
|
||||
"frequency": 50.7,
|
||||
"state": "enabled",
|
||||
"measurement_type": "pv3p",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L2": {
|
||||
"eid": "100000072",
|
||||
"timestamp": 1708006122,
|
||||
"energy_delivered": 712342,
|
||||
"energy_received": 723452,
|
||||
"active_power": 127,
|
||||
"power_factor": 0.27,
|
||||
"voltage": 117,
|
||||
"current": 7.2,
|
||||
"frequency": 50.7,
|
||||
"state": "enabled",
|
||||
"measurement_type": "pv3p",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
},
|
||||
"L3": {
|
||||
"eid": "100000073",
|
||||
"timestamp": 1708006123,
|
||||
"energy_delivered": 712343,
|
||||
"energy_received": 723453,
|
||||
"active_power": 137,
|
||||
"power_factor": 0.27,
|
||||
"voltage": 117,
|
||||
"current": 7.3,
|
||||
"frequency": 50.7,
|
||||
"state": "enabled",
|
||||
"measurement_type": "pv3p",
|
||||
"metering_status": "normal",
|
||||
"status_flags": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"ctmeter_production": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
from pyenphase.models.meters import CtType
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -118,6 +119,68 @@ async def test_entry_diagnostics_with_interface_information(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
# fix order of entities by device to avoid snapshot assertion
|
||||
# failures due to changed id based order between test runs
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
) == snapshot(exclude=limit_diagnostic_attrs)
|
||||
)
|
||||
diagnostics["envoy_entities_by_device"] = [
|
||||
{
|
||||
"device": device_entities["device"],
|
||||
"entities": sorted(
|
||||
device_entities["entities"], key=lambda e: e["entity"]["entity_id"]
|
||||
),
|
||||
}
|
||||
for device_entities in sorted(
|
||||
diagnostics["envoy_entities_by_device"],
|
||||
key=lambda e: e["device"]["identifiers"],
|
||||
)
|
||||
]
|
||||
assert diagnostics == snapshot(exclude=limit_diagnostic_attrs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy", "ctpresent"),
|
||||
[
|
||||
("envoy", ()),
|
||||
("envoy_1p_metered", (CtType.PRODUCTION, CtType.NET_CONSUMPTION)),
|
||||
("envoy_acb_batt", (CtType.PRODUCTION, CtType.NET_CONSUMPTION)),
|
||||
("envoy_eu_batt", (CtType.PRODUCTION, CtType.NET_CONSUMPTION)),
|
||||
(
|
||||
"envoy_metered_batt_relay",
|
||||
(
|
||||
CtType.PRODUCTION,
|
||||
CtType.NET_CONSUMPTION,
|
||||
CtType.STORAGE,
|
||||
CtType.BACKFEED,
|
||||
CtType.LOAD,
|
||||
CtType.EVSE,
|
||||
CtType.PV3P,
|
||||
),
|
||||
),
|
||||
("envoy_nobatt_metered_3p", (CtType.PRODUCTION, CtType.NET_CONSUMPTION)),
|
||||
("envoy_tot_cons_metered", (CtType.PRODUCTION, CtType.TOTAL_CONSUMPTION)),
|
||||
],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
async def test_entry_diagnostics_ct_presence(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_envoy: AsyncMock,
|
||||
ctpresent: tuple[CtType, ...],
|
||||
) -> None:
|
||||
"""Test config entry diagnostics including interface data."""
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
)
|
||||
# are expected ct in diagnostic report
|
||||
for ct in ctpresent:
|
||||
assert diagnostics["envoy_model_data"]["ctmeters"][ct]
|
||||
|
||||
# are no more ct in diagnostic report as in ctpresent
|
||||
for ct in diagnostics["envoy_model_data"]["ctmeters"]:
|
||||
assert ct in ctpresent
|
||||
|
||||
@@ -680,6 +680,186 @@ async def test_sensor_storage_ct_phase_data(
|
||||
assert entity_state.state == target
|
||||
|
||||
|
||||
CT_NAMES_FLOAT = (
|
||||
"<cttype>_ct_energy_delivered",
|
||||
"<cttype>_ct_energy_received",
|
||||
"<cttype>_ct_power",
|
||||
"frequency_<cttype>_ct",
|
||||
"voltage_<cttype>_ct",
|
||||
"<cttype>_ct_current",
|
||||
"power_factor_<cttype>_ct",
|
||||
"meter_status_flags_active_<cttype>_ct",
|
||||
)
|
||||
CT_NAMES_STR = ("metering_status_<cttype>_ct",)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cttype", "mock_envoy"),
|
||||
[
|
||||
(CtType.PRODUCTION, "envoy_metered_batt_relay"),
|
||||
(CtType.TOTAL_CONSUMPTION, "envoy_tot_cons_metered"),
|
||||
(CtType.BACKFEED, "envoy_metered_batt_relay"),
|
||||
(CtType.LOAD, "envoy_metered_batt_relay"),
|
||||
(CtType.EVSE, "envoy_metered_batt_relay"),
|
||||
(CtType.PV3P, "envoy_metered_batt_relay"),
|
||||
],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_ct_data(
|
||||
hass: HomeAssistant,
|
||||
mock_envoy: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
cttype: CtType,
|
||||
) -> None:
|
||||
"""Test ct entities values."""
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
sn = mock_envoy.serial_number
|
||||
ENTITY_BASE: str = f"{Platform.SENSOR}.envoy_{sn}"
|
||||
|
||||
data = mock_envoy.data.ctmeters[cttype]
|
||||
|
||||
CT_TARGETS_FLOAT = (
|
||||
data.energy_delivered / 1000000.0,
|
||||
data.energy_received / 1000000.0,
|
||||
data.active_power / 1000.0,
|
||||
data.frequency,
|
||||
data.voltage,
|
||||
data.current,
|
||||
data.power_factor,
|
||||
len(data.status_flags),
|
||||
)
|
||||
count_names: int = 0
|
||||
|
||||
for name, target in list(
|
||||
zip(
|
||||
[
|
||||
entity.replace("<cttype>", cttype).replace("-", "_")
|
||||
for entity in CT_NAMES_FLOAT
|
||||
],
|
||||
CT_TARGETS_FLOAT,
|
||||
strict=False,
|
||||
)
|
||||
):
|
||||
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}"))
|
||||
assert float(entity_state.state) == target
|
||||
count_names += 1
|
||||
|
||||
CT_TARGETS_STR = (data.metering_status,)
|
||||
for name, target in list(
|
||||
zip(
|
||||
[
|
||||
entity.replace("<cttype>", cttype).replace("-", "_")
|
||||
for entity in CT_NAMES_STR
|
||||
],
|
||||
CT_TARGETS_STR,
|
||||
strict=False,
|
||||
)
|
||||
):
|
||||
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}"))
|
||||
assert entity_state.state == target
|
||||
count_names += 1
|
||||
|
||||
# verify we're testing them all
|
||||
assert len(CT_NAMES_FLOAT) + len(CT_NAMES_STR) == count_names
|
||||
|
||||
|
||||
CT_NAMES_FLOAT_PHASE = [
|
||||
f"{name}_{phase.lower()}" for phase in PHASENAMES for name in (CT_NAMES_FLOAT)
|
||||
]
|
||||
|
||||
CT_NAMES_STR_PHASE = [
|
||||
f"{name}_{phase.lower()}" for phase in PHASENAMES for name in (CT_NAMES_STR)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cttype",
|
||||
[
|
||||
CtType.PRODUCTION,
|
||||
CtType.BACKFEED,
|
||||
CtType.LOAD,
|
||||
CtType.EVSE,
|
||||
CtType.PV3P,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy"),
|
||||
[
|
||||
"envoy_metered_batt_relay",
|
||||
],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_ct_phase_data(
|
||||
hass: HomeAssistant,
|
||||
mock_envoy: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
cttype: CtType,
|
||||
) -> None:
|
||||
"""Test ct phase entities values."""
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
sn = mock_envoy.serial_number
|
||||
ENTITY_BASE: str = f"{Platform.SENSOR}.envoy_{sn}"
|
||||
|
||||
CT_NAMES_FLOAT_PHASE_TARGET = chain(
|
||||
*[
|
||||
(
|
||||
phase_data.energy_delivered / 1000000.0,
|
||||
phase_data.energy_received / 1000000.0,
|
||||
phase_data.active_power / 1000.0,
|
||||
phase_data.frequency,
|
||||
phase_data.voltage,
|
||||
phase_data.current,
|
||||
phase_data.power_factor,
|
||||
len(phase_data.status_flags),
|
||||
)
|
||||
for phase_data in mock_envoy.data.ctmeters_phases[cttype].values()
|
||||
]
|
||||
)
|
||||
|
||||
count_names: int = 0
|
||||
for name, target in list(
|
||||
zip(
|
||||
[
|
||||
entity.replace("<cttype>", cttype).replace("-", "_")
|
||||
for entity in CT_NAMES_FLOAT_PHASE
|
||||
],
|
||||
CT_NAMES_FLOAT_PHASE_TARGET,
|
||||
strict=False,
|
||||
)
|
||||
):
|
||||
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}"))
|
||||
assert float(entity_state.state) == target
|
||||
count_names += 1
|
||||
|
||||
CT_NAMES_STR_PHASE_TARGET = [
|
||||
phase_data.metering_status
|
||||
for phase_data in mock_envoy.data.ctmeters_phases[cttype].values()
|
||||
]
|
||||
|
||||
for name, target in list(
|
||||
zip(
|
||||
[
|
||||
entity.replace("<cttype>", cttype).replace("-", "_")
|
||||
for entity in CT_NAMES_STR_PHASE
|
||||
],
|
||||
CT_NAMES_STR_PHASE_TARGET,
|
||||
strict=False,
|
||||
)
|
||||
):
|
||||
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}"))
|
||||
assert entity_state.state == target
|
||||
count_names += 1
|
||||
|
||||
# verify we're testing them all
|
||||
assert len(CT_NAMES_FLOAT_PHASE) + len(CT_NAMES_STR_PHASE) == count_names
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy"),
|
||||
[
|
||||
|
||||
@@ -19,9 +19,11 @@ from .conftest import MockESPHomeDeviceType
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration", "mock_dashboard")
|
||||
@pytest.mark.usefixtures("mock_dashboard")
|
||||
async def test_dashboard_storage(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
init_integration: MockConfigEntry,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test dashboard storage."""
|
||||
@@ -129,6 +131,7 @@ async def test_setup_dashboard_fails(
|
||||
|
||||
async def test_setup_dashboard_fails_when_already_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -168,7 +171,9 @@ async def test_setup_dashboard_fails_when_already_setup(
|
||||
|
||||
@pytest.mark.usefixtures("mock_dashboard")
|
||||
async def test_new_info_reload_config_entries(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entries are reloaded when new info is set."""
|
||||
assert init_integration.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from evohomeasync2 import EvohomeClient
|
||||
@@ -18,10 +19,11 @@ from homeassistant.components.evohome.const import (
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
async def test_service_refresh_system(
|
||||
async def test_refresh_system(
|
||||
hass: HomeAssistant,
|
||||
evohome: EvohomeClient,
|
||||
) -> None:
|
||||
@@ -40,7 +42,7 @@ async def test_service_refresh_system(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
async def test_service_reset_system(
|
||||
async def test_reset_system(
|
||||
hass: HomeAssistant,
|
||||
ctl_id: str,
|
||||
) -> None:
|
||||
@@ -59,7 +61,7 @@ async def test_service_reset_system(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
async def test_ctl_set_system_mode(
|
||||
async def test_set_system_mode(
|
||||
hass: HomeAssistant,
|
||||
ctl_id: str,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@@ -115,7 +117,7 @@ async def test_ctl_set_system_mode(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
async def test_zone_clear_zone_override(
|
||||
async def test_clear_zone_override(
|
||||
hass: HomeAssistant,
|
||||
zone_id: str,
|
||||
) -> None:
|
||||
@@ -126,9 +128,8 @@ async def test_zone_clear_zone_override(
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
EvoService.CLEAR_ZONE_OVERRIDE,
|
||||
{
|
||||
ATTR_ENTITY_ID: zone_id,
|
||||
},
|
||||
{},
|
||||
target={ATTR_ENTITY_ID: zone_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -136,7 +137,7 @@ async def test_zone_clear_zone_override(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
async def test_zone_set_zone_override(
|
||||
async def test_set_zone_override(
|
||||
hass: HomeAssistant,
|
||||
zone_id: str,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@@ -151,9 +152,9 @@ async def test_zone_set_zone_override(
|
||||
DOMAIN,
|
||||
EvoService.SET_ZONE_OVERRIDE,
|
||||
{
|
||||
ATTR_ENTITY_ID: zone_id,
|
||||
ATTR_SETPOINT: 19.5,
|
||||
},
|
||||
target={ATTR_ENTITY_ID: zone_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -165,13 +166,41 @@ async def test_zone_set_zone_override(
|
||||
DOMAIN,
|
||||
EvoService.SET_ZONE_OVERRIDE,
|
||||
{
|
||||
ATTR_ENTITY_ID: zone_id,
|
||||
ATTR_SETPOINT: 19.5,
|
||||
ATTR_DURATION: {"minutes": 135},
|
||||
},
|
||||
target={ATTR_ENTITY_ID: zone_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_fcn.assert_awaited_once_with(
|
||||
19.5, until=datetime(2024, 7, 10, 14, 15, tzinfo=UTC)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", ["default"])
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data"),
|
||||
[
|
||||
(EvoService.CLEAR_ZONE_OVERRIDE, {}),
|
||||
(EvoService.SET_ZONE_OVERRIDE, {ATTR_SETPOINT: 19.5}),
|
||||
],
|
||||
)
|
||||
async def test_zone_services_with_ctl_id(
|
||||
hass: HomeAssistant,
|
||||
ctl_id: str,
|
||||
service: EvoService,
|
||||
service_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test calling zone-only services with a non-zone entity_id fail."""
|
||||
|
||||
with pytest.raises(ServiceValidationError) as excinfo:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
service_data,
|
||||
target={ATTR_ENTITY_ID: ctl_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert excinfo.value.translation_key == "zone_only_service"
|
||||
|
||||
@@ -76,6 +76,10 @@ async def test_config_flow(hass: HomeAssistant, config_entry: MockConfigEntry) -
|
||||
"homeassistant.components.forked_daapd.ForkedDaapdAPI.get_request",
|
||||
autospec=True,
|
||||
) as mock_get_request,
|
||||
patch(
|
||||
"homeassistant.components.forked_daapd.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
mock_get_request.return_value = SAMPLE_CONFIG
|
||||
mock_test_connection.return_value = ["ok", "My Music on myhost"]
|
||||
@@ -229,10 +233,16 @@ async def test_config_flow_zeroconf_valid(hass: HomeAssistant) -> None:
|
||||
async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test config flow options."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.forked_daapd.ForkedDaapdAPI.get_request",
|
||||
autospec=True,
|
||||
) as mock_get_request:
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.forked_daapd.ForkedDaapdAPI.get_request",
|
||||
autospec=True,
|
||||
) as mock_get_request,
|
||||
patch(
|
||||
"homeassistant.components.forked_daapd.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
mock_get_request.return_value = SAMPLE_CONFIG
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
@@ -168,6 +168,7 @@ async def test_duplicate_updates_existing_entry(
|
||||
async def test_dhcp_discovery_updates_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test DHCP discovery updates config entries."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
@@ -104,6 +104,8 @@ def mock_hdfury_client() -> Generator[AsyncMock]:
|
||||
"relay": "0",
|
||||
"macaddr": "c7:1c:df:9d:f6:40",
|
||||
"reboottimer": "0",
|
||||
"unmutecnt": "500",
|
||||
"earcunmutecnt": "0",
|
||||
"oled": "1",
|
||||
"oledfade": "30",
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
'cec1en': '1',
|
||||
'cec2en': '1',
|
||||
'cec3en': '1',
|
||||
'earcunmutecnt': '0',
|
||||
'htpcmode0': '0',
|
||||
'htpcmode1': '0',
|
||||
'htpcmode2': '0',
|
||||
@@ -29,6 +30,7 @@
|
||||
'relay': '0',
|
||||
'tx0plus5': '1',
|
||||
'tx1plus5': '1',
|
||||
'unmutecnt': '500',
|
||||
}),
|
||||
'info': dict({
|
||||
'AUD0': 'bitstream 48kHz',
|
||||
|
||||
@@ -1,4 +1,64 @@
|
||||
# serializer version: 1
|
||||
# name: test_number_entities[number.hdfury_vrroom_02_earc_unmute_delay-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 1000,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.hdfury_vrroom_02_earc_unmute_delay',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'eARC unmute delay',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'eARC unmute delay',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'earc_unmute',
|
||||
'unique_id': '000123456789_earcunmutecnt',
|
||||
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.hdfury_vrroom_02_earc_unmute_delay-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'HDFury VRROOM-02 eARC unmute delay',
|
||||
'max': 1000,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.hdfury_vrroom_02_earc_unmute_delay',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.hdfury_vrroom_02_oled_fade_timer-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -119,3 +179,63 @@
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.hdfury_vrroom_02_unmute_delay-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 1000,
|
||||
'min': 50,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.hdfury_vrroom_02_unmute_delay',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Unmute delay',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Unmute delay',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'audio_unmute',
|
||||
'unique_id': '000123456789_unmutecnt',
|
||||
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.hdfury_vrroom_02_unmute_delay-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'HDFury VRROOM-02 Unmute delay',
|
||||
'max': 1000,
|
||||
'min': 50,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.hdfury_vrroom_02_unmute_delay',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '500.0',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -23,6 +23,7 @@ from . import setup_integration
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
@@ -40,8 +41,11 @@ async def test_number_entities(
|
||||
[
|
||||
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
|
||||
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
|
||||
("number.hdfury_vrroom_02_unmute_delay", "set_audio_unmute"),
|
||||
("number.hdfury_vrroom_02_earc_unmute_delay", "set_earc_unmute"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_set_value(
|
||||
hass: HomeAssistant,
|
||||
mock_hdfury_client: AsyncMock,
|
||||
@@ -68,8 +72,11 @@ async def test_number_set_value(
|
||||
[
|
||||
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
|
||||
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
|
||||
("number.hdfury_vrroom_02_unmute_delay", "set_audio_unmute"),
|
||||
("number.hdfury_vrroom_02_earc_unmute_delay", "set_earc_unmute"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_error(
|
||||
hass: HomeAssistant,
|
||||
mock_hdfury_client: AsyncMock,
|
||||
@@ -100,8 +107,11 @@ async def test_number_error(
|
||||
[
|
||||
("number.hdfury_vrroom_02_oled_fade_timer"),
|
||||
("number.hdfury_vrroom_02_restart_timer"),
|
||||
("number.hdfury_vrroom_02_unmute_delay"),
|
||||
("number.hdfury_vrroom_02_earc_unmute_delay"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_entities_unavailable_on_error(
|
||||
hass: HomeAssistant,
|
||||
mock_hdfury_client: AsyncMock,
|
||||
|
||||
@@ -336,7 +336,13 @@ async def test_entity_migration(
|
||||
config_entry=config_entry_v1_1,
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.home_connect.PLATFORMS", platforms):
|
||||
with (
|
||||
patch("homeassistant.components.home_connect.PLATFORMS", platforms),
|
||||
patch(
|
||||
"homeassistant.components.home_connect.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry_v1_1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -364,8 +370,12 @@ async def test_config_entry_unique_id_migration(
|
||||
assert config_entry_v1_2.unique_id != "1234567890"
|
||||
assert config_entry_v1_2.minor_version == 2
|
||||
|
||||
await hass.config_entries.async_setup(config_entry_v1_2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.home_connect.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry_v1_2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry_v1_2.unique_id == "1234567890"
|
||||
assert config_entry_v1_2.minor_version == 3
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The tests for the Homematic notification platform."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -9,11 +11,15 @@ from tests.common import assert_setup_component
|
||||
|
||||
async def test_setup_full(hass: HomeAssistant) -> None:
|
||||
"""Test valid configuration."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"homematic",
|
||||
{"homematic": {"hosts": {"ccu2": {"host": "127.0.0.1"}}}},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.homematic.HMConnection",
|
||||
return_value=MagicMock(),
|
||||
):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"homematic",
|
||||
{"homematic": {"hosts": {"ccu2": {"host": "127.0.0.1"}}}},
|
||||
)
|
||||
with assert_setup_component(1, domain="notify") as handle_config:
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
@@ -35,11 +41,15 @@ async def test_setup_full(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_setup_without_optional(hass: HomeAssistant) -> None:
|
||||
"""Test valid configuration without optional."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"homematic",
|
||||
{"homematic": {"hosts": {"ccu2": {"host": "127.0.0.1"}}}},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.homematic.HMConnection",
|
||||
return_value=MagicMock(),
|
||||
):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"homematic",
|
||||
{"homematic": {"hosts": {"ccu2": {"host": "127.0.0.1"}}}},
|
||||
)
|
||||
with assert_setup_component(1, domain="notify") as handle_config:
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
|
||||
@@ -131,10 +131,15 @@ def simple_mock_home_fixture():
|
||||
get_current_state_async=AsyncMock(),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
|
||||
autospec=True,
|
||||
return_value=mock_home,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
|
||||
autospec=True,
|
||||
return_value=mock_home,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homematicip.connection.connection_context import ConnectionContext
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
from homeassistant.components.homematicip_cloud.const import (
|
||||
@@ -107,7 +106,6 @@ async def test_load_entry_fails_due_to_connection_error(
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
|
||||
return_value=ConnectionContext(),
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
@@ -127,6 +125,9 @@ async def test_load_entry_fails_due_to_generic_exception(
|
||||
"homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state_async",
|
||||
side_effect=Exception,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_http_server() -> None:
|
||||
def disable_http_server(socket_enabled: None) -> None:
|
||||
"""Override the global disable_http_server fixture with an empty fixture.
|
||||
|
||||
This allows the HTTP server to start in tests that need it.
|
||||
|
||||
@@ -136,6 +136,7 @@ def fixture_mock_config_entry() -> MockConfigEntry:
|
||||
async def fixture_mock_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device: MagicMock,
|
||||
) -> MockConfigEntry:
|
||||
"""Return a mock ConfigEntry setup for the integration."""
|
||||
with (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for JVC Projector config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError
|
||||
import pytest
|
||||
@@ -18,6 +19,16 @@ from tests.common import MockConfigEntry
|
||||
TARGET = "homeassistant.components.jvc_projector.config_flow.JvcProjector"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.jvc_projector.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True)
|
||||
async def test_user_config_flow_success(
|
||||
hass: HomeAssistant, mock_device: AsyncMock
|
||||
|
||||
@@ -400,14 +400,18 @@ async def test_reconfigure(
|
||||
return_value={"scb:network": {"Hostname": "scb"}}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1")
|
||||
mock_apiclient.__aenter__.assert_called_once()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the LaMetric config flow."""
|
||||
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from demetriek import (
|
||||
LaMetricConnectionError,
|
||||
@@ -686,6 +686,7 @@ async def test_cloud_errors(
|
||||
async def test_dhcp_discovery_updates_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test DHCP discovery updates config entries."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
@@ -90,6 +90,7 @@ async def test_device_already_configured(
|
||||
async def test_user_step_fail_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
@@ -124,6 +125,7 @@ async def test_user_step_fail_with_error(
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
@@ -153,6 +155,7 @@ async def test_reconfigure(
|
||||
async def test_reconfigure_fail_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -20,6 +20,7 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
async def test_binary_sensor_setup(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_madvr_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user