Compare commits

..

71 Commits

Author SHA1 Message Date
Klaas Schoute
642864959a Update translatable exceptions for Powerfox integration (#164322) 2026-02-28 01:57:02 +00:00
Franck Nijhof
7ef6c34149 Reject relative paths in SFTP storage backup location config flow (#164408) 2026-02-27 19:25:04 -05:00
Franck Nijhof
5b32e42b8c Add aioclient_mock to ssdp tests to prevent real HTTP requests (#164403) 2026-02-27 19:24:13 -05:00
Franck Nijhof
1be8b8e525 Add discovery mocks to tplink init tests (#164386) 2026-02-27 19:23:47 -05:00
Franck Nijhof
3fae15c430 Fix fixture ordering in esphome dashboard tests (#164367) 2026-02-27 19:23:13 -05:00
Franck Nijhof
c7e78568d0 Enable real sockets in default_config setup test (#164366) 2026-02-27 19:22:29 -05:00
Stefan Agner
492b542136 Fix Matter vacuum crash on nullable ServiceArea location info (#164411) 2026-02-28 00:11:32 +01:00
Franck Nijhof
0f4852d8c2 Enable sockets for http integration tests (#164404) 2026-02-27 22:22:15 +01:00
nopoz
737c0c1823 Google Cast: detect state and attributes when device is doing active non-media casting (#160819)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-02-27 22:07:09 +01:00
Petro31
5fadcb01e9 Fix int vs float template sensor issue (#164339) 2026-02-27 22:06:37 +01:00
TheJulianJES
2b4f46a739 Fix ZHA update entities not working after reload (#164290) 2026-02-27 22:04:51 +01:00
Franck Nijhof
44fe37da1f Mock ConnectionContextBuilder in homematicip_cloud tests (#164356) 2026-02-27 22:00:37 +01:00
Joost Lekkerkerker
abd4e89577 Sync SmartThings vacuum fixture (#164360) 2026-02-27 21:43:30 +01:00
Franck Nijhof
033798835a Refactor adguard tests to use proper fixtures for mocking (#164402)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 21:34:10 +01:00
Franck Nijhof
83c77957c1 Add missing mock fixtures to telegram_bot polling init test (#164398) 2026-02-27 21:29:10 +01:00
dependabot[bot]
b1bc1dc102 Bump actions/dependency-review-action from 4.8.2 to 4.8.3 (#164296)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 21:21:15 +01:00
Jason Hunter
40b8a2c380 Remove Duke Energy (#164282)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-27 20:19:03 +00:00
Glenn de Haan
fb23a6fbf8 Add HDFury audio offset numbers (#164315) 2026-02-27 21:02:34 +01:00
Joost Lekkerkerker
faad3de02c Bump pySmartThings to 3.6.0 (#164397) 2026-02-27 21:00:33 +01:00
Franck Nijhof
5f30f532e5 Mock async_setup_entry in unifiprotect reauth tests (#164375) 2026-02-27 20:53:52 +01:00
Franck Nijhof
667e8c4d38 Mock async_setup_entry in jvc_projector config flow tests (#164401) 2026-02-27 20:53:38 +01:00
Franck Nijhof
74240ecd26 Mock async_setup_entry in lametric DHCP discovery test (#164400) 2026-02-27 20:50:11 +01:00
Franck Nijhof
c81ee53265 Mock TodoistAPIAsync in todoist failed coordinator update test (#164390) 2026-02-27 20:49:02 +01:00
Franck Nijhof
8835f1d5e6 Mock async_setup_entry in youless config flow test (#164399) 2026-02-27 20:46:48 +01:00
Franck Nijhof
2ca84182d8 Patch discovery in elkm1 invalid auth and reconfigure tests (#164396) 2026-02-27 20:46:45 +01:00
Franck Nijhof
3f0d1bc071 Mock PyMochad controller in mochad tests (#164394) 2026-02-27 20:43:08 +01:00
Franck Nijhof
350f462bdf Prevent real setup during DHCP discovery test in fully_kiosk tests (#164342) 2026-02-27 20:42:32 +01:00
Franck Nijhof
2f98e68ed8 Mock async_setup_entry in arcam_fmj config flow tests (#164351) 2026-02-27 20:42:12 +01:00
Franck Nijhof
5b7fac94e5 Mock async_setup_entry in ccm15 config flow tests (#164352) 2026-02-27 20:42:02 +01:00
Franck Nijhof
c32ce3da5c Add missing rest_api fixture in samsungtv setup test (#164353) 2026-02-27 20:41:39 +01:00
Franck Nijhof
0e1d1fbaed Fix fixture ordering in jvc_projector integration setup (#164354) 2026-02-27 20:41:17 +01:00
Franck Nijhof
57d7f364f4 Mock async_setup_entry in wilight SSDP flow test (#164393) 2026-02-27 20:40:35 +01:00
Franck Nijhof
7cc5777b47 Fix fixture ordering in madVR tests to ensure proper mocking (#164350) 2026-02-27 20:38:42 +01:00
Franck Nijhof
5e3f23b6a2 Fix mock target for Met Office config flow error test (#164391) 2026-02-27 20:37:24 +01:00
Franck Nijhof
6873a40407 Mock async_setup_entry in forked_daapd config flow tests (#164370) 2026-02-27 20:36:33 +01:00
Franck Nijhof
ddaa2fb293 Mock async_setup_entry in daikin config flow tests (#164371) 2026-02-27 20:36:23 +01:00
Franck Nijhof
53b6223459 Mock async_setup_entry in emulated_roku config flow tests (#164368) 2026-02-27 20:35:50 +01:00
Franck Nijhof
7329cfb927 Mock async_setup_entry in home_connect migration tests (#164357) 2026-02-27 20:33:54 +01:00
Franck Nijhof
44b80dde0c Mock async_setup_entry in radarr config flow tests (#164359) 2026-02-27 20:33:19 +01:00
Joost Lekkerkerker
8c125e4e4f Add do not disturb switch to SmartThings (#164364) 2026-02-27 20:31:56 +01:00
Franck Nijhof
227a258382 Add missing client mocks to tplink_omada service tests (#164389) 2026-02-27 20:30:54 +01:00
Franck Nijhof
addc2a6766 Mock async_setup_entry in speedtestdotnet config flow test (#164387) 2026-02-27 20:30:47 +01:00
Franck Nijhof
97bcea9727 Mock async_setup_entry in tautulli config flow tests (#164388) 2026-02-27 20:30:38 +01:00
Franck Nijhof
4f05c807b0 Mock async_setup_entry in panasonic_viera config flow tests (#164385) 2026-02-27 20:30:25 +01:00
Franck Nijhof
177a918c26 Mock async_setup_entry in onvif DHCP host update test (#164384) 2026-02-27 20:30:15 +01:00
Franck Nijhof
9705770c6c Remove unnecessary config entry from velux validation error test (#164383) 2026-02-27 20:30:12 +01:00
Franck Nijhof
7309351165 Mock async_setup_entry in lunatone config flow tests (#164382) 2026-02-27 20:29:22 +01:00
Franck Nijhof
d0401de70d Mock HMConnection in homematic notify tests (#164381) 2026-02-27 20:29:14 +01:00
Franck Nijhof
6b89359a73 Mock async_setup_entry in sharkiq setup test (#164380) 2026-02-27 20:27:40 +01:00
Franck Nijhof
b31bafab99 Mock async_setup_entry in roku options flow test (#164377) 2026-02-27 20:27:13 +01:00
Franck Nijhof
84c556bb63 Mock setup and client in sma config flow tests (#164374) 2026-02-27 20:26:59 +01:00
Franck Nijhof
225ea02d9a Fix axis setup failure test to mock at correct layer (#164373) 2026-02-27 20:26:46 +01:00
Franck Nijhof
ebd1cc994c Add missing mock_transmission_client to transmission init tests (#164369) 2026-02-27 20:26:33 +01:00
Franck Nijhof
9ec22ba158 Mock async_setup_entry in kostal_plenticore reconfigure test (#164372) 2026-02-27 20:26:18 +01:00
Paulus Schoutsen
2ff85d2134 Add missing volume supported features to dunehd (#164343)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:50:42 +01:00
reneboer
3eb7f04510 Add tests for Megane e-Tech (#164358)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 19:47:22 +01:00
Kamil Breguła
54613ac8d9 Add mik-laj as codeowner to WLED (#164349)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2026-02-27 18:31:37 +01:00
Joost Lekkerkerker
044522a8ab Add state for washing mop in SmartThings (#164348) 2026-02-27 18:26:20 +01:00
Willem-Jan van Rootselaar
19bf41496a Set entity_registry_enabled_default to False for total energy sensor (#164197) 2026-02-27 18:03:17 +01:00
Johnny Willemsen
a7efba098d Update state labels to use common keys in indevolt (#164308) 2026-02-27 17:57:02 +01:00
Arie Catsman
042ad3b759 Add missing production ct data, total-consumption and new CT to enphase_envoy (#164270) 2026-02-27 17:43:46 +01:00
Franck Nijhof
4270e4c793 Mock firmware data during reauth flow init in airos tests (#164341) 2026-02-27 17:21:22 +01:00
Erwin Douna
cb11c22e76 SMA add data descriptions (#164331) 2026-02-27 16:34:45 +01:00
Norbert Rittel
c6e23fec93 Replace "service" with "action" in evohome exception string (#164333) 2026-02-27 16:32:15 +01:00
epenet
553cecb397 Ensure future is marked as retrieved in frontend storage (#164320) 2026-02-27 15:51:34 +02:00
Erwin Douna
bb7d5897d1 Portainer redact CONF_HOST in diagnostics (#164301) 2026-02-27 13:54:12 +01:00
7eaves
3e050ebe59 Bump PySwitchBot to 1.1.0 (#164298) 2026-02-27 13:11:14 +01:00
Ye Zhiling
856a9e695a Pass encoding to AtomicWriter in write_utf8_file_atomic (#164015) 2026-02-27 11:40:58 +01:00
Artur Pragacz
1944a8bd3a Remove vacuum area mapping not configured issue (#164259) 2026-02-27 11:20:46 +01:00
epenet
3f11af8084 Drop single-use service name constants in bsblan (#164311) 2026-02-27 10:59:02 +01:00
David Bonnes
46a87cd9dd Migrate evohome's zone services to entity-level services (#164105)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-27 10:16:35 +01:00
156 changed files with 30817 additions and 8621 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
"""Constants for the Duke Energy integration."""
DOMAIN = "duke_energy"

View File

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

View File

@@ -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"]
}

View File

@@ -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%]"
}
}
}
}
}

View File

@@ -33,6 +33,8 @@ DUNEHD_PLAYER_SUPPORT: Final[MediaPlayerEntityFeature] = (
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
)

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,12 @@
}
},
"number": {
"audio_unmute": {
"default": "mdi:volume-high"
},
"earc_unmute": {
"default": "mdi:volume-high"
},
"oled_fade": {
"default": "mdi:cellphone-information"
},

View File

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

View File

@@ -41,6 +41,12 @@
}
},
"number": {
"audio_unmute": {
"name": "Unmute delay"
},
"earc_unmute": {
"name": "eARC unmute delay"
},
"oled_fade": {
"name": "OLED fade timer"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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."
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,5 +34,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.5.3"]
"requirements": ["pysmartthings==3.6.0"]
}

View File

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

View File

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

View File

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

View File

@@ -42,5 +42,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==1.0.0"]
"requirements": ["PySwitchbot==1.1.0"]
}

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

@@ -161,7 +161,6 @@ FLOWS = {
"dsmr",
"dsmr_reader",
"duckdns",
"duke_energy",
"dunehd",
"duotecno",
"dwd_weather_warnings",

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
"""Tests for the Duke Energy integration."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
})
# ---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'on',
})
# ---

View File

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