From 8912b19cf4c36278419e63b7263c858b640bbb0c Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 11 Aug 2023 20:15:42 -0400 Subject: [PATCH 01/13] Add Enphase Encharge aggregate sensors (#98276) * Add Encharge aggregate sensors * Update dependency --- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 71 +++++++++++++++++++ .../components/enphase_envoy/strings.json | 15 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index f500ac538e7..6969dc3d6ab 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.4.0"], + "requirements": ["pyenphase==1.5.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 9ecc205522a..0e4a9b71232 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -8,6 +8,7 @@ import logging from pyenphase import ( EnvoyEncharge, + EnvoyEnchargeAggregate, EnvoyEnchargePower, EnvoyEnpower, EnvoyInverter, @@ -288,6 +289,58 @@ ENPOWER_SENSORS = ( ) +@dataclass +class EnvoyEnchargeAggregateRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnchargeAggregate], int] + + +@dataclass +class EnvoyEnchargeAggregateSensorEntityDescription( + SensorEntityDescription, EnvoyEnchargeAggregateRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +ENCHARGE_AGGREGATE_SENSORS = ( + EnvoyEnchargeAggregateSensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda encharge: encharge.state_of_charge, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="reserve_soc", + translation_key="reserve_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda encharge: encharge.reserve_state_of_charge, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="available_energy", + translation_key="available_energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.available_energy, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="reserve_energy", + translation_key="reserve_energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.backup_reserve, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="max_capacity", + translation_key="max_capacity", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.max_available_capacity, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -329,6 +382,11 @@ async def async_setup_entry( for description in ENCHARGE_POWER_SENSORS for encharge in envoy_data.encharge_power ) + if envoy_data.encharge_aggregate: + entities.extend( + EnvoyEnchargeAggregateEntity(coordinator, description) + for description in ENCHARGE_AGGREGATE_SENSORS + ) if envoy_data.enpower: entities.extend( EnvoyEnpowerEntity(coordinator, description) @@ -482,6 +540,19 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): return self.entity_description.value_fn(encharge_power[self._serial_number]) +class EnvoyEnchargeAggregateEntity(EnvoySystemSensorEntity): + """Envoy Encharge Aggregate sensor entity.""" + + entity_description: EnvoyEnchargeAggregateSensorEntityDescription + + @property + def native_value(self) -> int: + """Return the state of the aggregate sensors.""" + encharge_aggregate = self.data.encharge_aggregate + assert encharge_aggregate is not None + return self.entity_description.value_fn(encharge_aggregate) + + class EnvoyEnpowerEntity(EnvoySensorBaseEntity): """Envoy Enpower sensor entity.""" diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index f42e44d7afa..2afd19d87d1 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -63,6 +63,21 @@ }, "lifetime_consumption": { "name": "Lifetime energy consumption" + }, + "reserve_soc": { + "name": "Reserve battery level" + }, + "available_energy": { + "name": "Available battery energy" + }, + "reserve_energy": { + "name": "Reserve battery energy" + }, + "max_capacity": { + "name": "Battery capacity" + }, + "configured_reserve_soc": { + "name": "Configured reserve battery level" } }, "switch": { diff --git a/requirements_all.txt b/requirements_all.txt index 4f99f44c9f1..8eae7bc175f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1665,7 +1665,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.4.0 +pyenphase==1.5.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc1bc38b233..7048126d3a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.4.0 +pyenphase==1.5.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 8b99d4678f54c439435137c19fec6e7ea354f8de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Aug 2023 09:10:25 +0200 Subject: [PATCH 02/13] Correct checks for non-finite numbers in ESPHome (#98102) --- homeassistant/components/esphome/entity.py | 4 ++-- homeassistant/components/esphome/number.py | 2 +- homeassistant/components/esphome/sensor.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 57ae33beb15..8b69d011804 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -105,8 +105,8 @@ def esphome_state_property( if not self._has_state: return None val = func(self) - if isinstance(val, float) and math.isnan(val): - # Home Assistant doesn't use NAN values in state machine + if isinstance(val, float) and not math.isfinite(val): + # Home Assistant doesn't use NaN or inf values in state machine # (not JSON serializable) return None return val diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 4f3109f5a83..bc694ec39cf 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -73,7 +73,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): def native_value(self) -> float | None: """Return the state of the entity.""" state = self._state - if state.missing_state or math.isnan(state.state): + if state.missing_state or not math.isfinite(state.state): return None return state.state diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index af873565fc3..efc77ff53b8 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -96,7 +96,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): def native_value(self) -> datetime | str | None: """Return the state of the entity.""" state = self._state - if math.isnan(state.state) or state.missing_state: + if state.missing_state or not math.isfinite(state.state): return None if self._attr_device_class == SensorDeviceClass.TIMESTAMP: return dt_util.utc_from_timestamp(state.state) From 5042c25bbc84dbc8910c7f171ec4793c749cdf71 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 12 Aug 2023 09:56:23 +0200 Subject: [PATCH 03/13] Plugwise climate: remove extra_state_attributes property (#98153) * Remove extra_state_attributes property, replaced by a number * Support HVAC_Mode in set_temperature() * Remove set_temperature() update, as requested --- homeassistant/components/plugwise/climate.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 5be09a062e2..e83b76a76da 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -1,7 +1,6 @@ """Plugwise Climate component for Home Assistant.""" from __future__ import annotations -from collections.abc import Mapping from typing import Any from homeassistant.components.climate import ( @@ -145,14 +144,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Return the current preset mode.""" return self.device.get("active_preset") - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return { - "available_schemas": self.device["available_schedules"], - "selected_schema": self.device["select_schedule"], - } - @plugwise_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From d6498aa39e40f5aa34707533d440736f7b19b963 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sat, 12 Aug 2023 10:27:45 +0200 Subject: [PATCH 04/13] Fix fanSpeed issue (#98293) --- homeassistant/components/tado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index b57d384124c..0ef6dc17934 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -327,7 +327,7 @@ class TadoConnector: device_type, "ON", mode, - fanSpeed=fan_speed, + fan_speed=fan_speed, swing=swing, ) From be9afd7eae85f567e3bee6e99ff4f8ca625a4261 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 12 Aug 2023 11:03:37 +0200 Subject: [PATCH 05/13] Add entity translations to DWD (#98254) * Add device to DWD * Add entity translations to DWD --- .../components/dwd_weather_warnings/sensor.py | 6 +++--- .../components/dwd_weather_warnings/strings.json | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 7bc683d245d..78154e9e4f4 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -56,12 +56,12 @@ from .coordinator import DwdWeatherWarningsCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=CURRENT_WARNING_SENSOR, - name="Current Warning Level", + translation_key=CURRENT_WARNING_SENSOR, icon="mdi:close-octagon-outline", ), SensorEntityDescription( key=ADVANCE_WARNING_SENSOR, - name="Advance Warning Level", + translation_key=ADVANCE_WARNING_SENSOR, icon="mdi:close-octagon-outline", ), ) @@ -131,6 +131,7 @@ class DwdWeatherWarningsSensor( """Representation of a DWD-Weather-Warnings sensor.""" _attr_attribution = "Data provided by DWD" + _attr_has_entity_name = True def __init__( self, @@ -142,7 +143,6 @@ class DwdWeatherWarningsSensor( super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {entry.title} {description.name}" self._attr_unique_id = f"{entry.unique_id}-{description.key}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index 60e53f90dbd..dc73055174b 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -15,5 +15,15 @@ "already_configured": "Warncell ID / name is already configured.", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } + }, + "entity": { + "sensor": { + "current_warning_level": { + "name": "Current warning level" + }, + "advance_warning_level": { + "name": "Advance warning level" + } + } } } From ae8f9dcb7749dcd8646f4904fb026a1dbfa8539f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Aug 2023 15:15:09 +0200 Subject: [PATCH 06/13] Modernize ipma weather (#98062) * Modernize ipma weather * Add test snapshots * Don't include forecast mode in weather entity unique_id for new config entries * Remove old migration code * Remove outdated test --- homeassistant/components/ipma/config_flow.py | 5 +- homeassistant/components/ipma/const.py | 2 - homeassistant/components/ipma/weather.py | 115 ++++++++++++------ .../ipma/snapshots/test_weather.ambr | 104 ++++++++++++++++ tests/components/ipma/test_config_flow.py | 60 +-------- tests/components/ipma/test_weather.py | 100 ++++++++++++++- 6 files changed, 282 insertions(+), 104 deletions(-) create mode 100644 tests/components/ipma/snapshots/test_weather.ambr diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index eb361d3f9d5..9434aed3097 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -2,10 +2,10 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, FORECAST_MODE, HOME_LOCATION_NAME +from .const import DOMAIN, HOME_LOCATION_NAME class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -47,7 +47,6 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_NAME, default=name): str, vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, - vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ), errors=self._errors, diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 2d715011e43..c7482770f48 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -49,6 +49,4 @@ CONDITION_CLASSES = { ATTR_CONDITION_CLEAR_NIGHT: [-1], } -FORECAST_MODE = ["hourly", "daily"] - ATTRIBUTION = "Instituto Português do Mar e Atmosfera" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 811eddf91bf..b8e994a7500 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,11 +1,14 @@ """Support for IPMA weather service.""" from __future__ import annotations +import asyncio +import contextlib import logging +from typing import Literal import async_timeout from pyipma.api import IPMA_API -from pyipma.forecast import Forecast +from pyipma.forecast import Forecast as IPMAForecast from pyipma.location import Location from homeassistant.components.weather import ( @@ -16,7 +19,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,8 +31,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle @@ -53,39 +57,19 @@ async def async_setup_entry( """Add a weather entity from a config_entry.""" api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] - mode = config_entry.data[CONF_MODE] - - # Migrate old unique_id - @callback - def _async_migrator(entity_entry: er.RegistryEntry): - # Reject if new unique_id - if entity_entry.unique_id.count(",") == 2: - return None - - new_unique_id = ( - f"{location.station_latitude}, {location.station_longitude}, {mode}" - ) - - _LOGGER.info( - "Migrating unique_id from [%s] to [%s]", - entity_entry.unique_id, - new_unique_id, - ) - return {"new_unique_id": new_unique_id} - - await er.async_migrate_entries(hass, config_entry.entry_id, _async_migrator) - async_add_entities([IPMAWeather(location, api, config_entry.data)], True) class IPMAWeather(WeatherEntity, IPMADevice): """Representation of a weather condition.""" + _attr_attribution = ATTRIBUTION _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - - _attr_attribution = ATTRIBUTION + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__(self, location: Location, api: IPMA_API, config) -> None: """Initialise the platform with a data instance and station name.""" @@ -95,25 +79,35 @@ class IPMAWeather(WeatherEntity, IPMADevice): self._mode = config.get(CONF_MODE) self._period = 1 if config.get(CONF_MODE) == "hourly" else 24 self._observation = None - self._forecast: list[Forecast] = [] - self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" + self._daily_forecast: list[IPMAForecast] | None = None + self._hourly_forecast: list[IPMAForecast] | None = None + if self._mode is not None: + self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" + else: + self._attr_unique_id = ( + f"{self._location.station_latitude}, {self._location.station_longitude}" + ) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update Condition and Forecast.""" async with async_timeout.timeout(10): new_observation = await self._location.observation(self._api) - new_forecast = await self._location.forecast(self._api, self._period) if new_observation: self._observation = new_observation else: _LOGGER.warning("Could not update weather observation") - if new_forecast: - self._forecast = new_forecast + if self._period == 24 or self._forecast_listeners["daily"]: + await self._update_forecast("daily", 24, True) else: - _LOGGER.warning("Could not update weather forecast") + self._daily_forecast = None + + if self._period == 1 or self._forecast_listeners["hourly"]: + await self._update_forecast("hourly", 1, True) + else: + self._hourly_forecast = None _LOGGER.debug( "Updated location %s based on %s, current observation %s", @@ -122,6 +116,21 @@ class IPMAWeather(WeatherEntity, IPMADevice): self._observation, ) + async def _update_forecast( + self, + forecast_type: Literal["daily", "hourly"], + period: int, + update_listeners: bool, + ) -> None: + """Update weather forecast.""" + new_forecast = await self._location.forecast(self._api, period) + if new_forecast: + setattr(self, f"_{forecast_type}_forecast", new_forecast) + if update_listeners: + await self.async_update_listeners((forecast_type,)) + else: + _LOGGER.warning("Could not update %s weather forecast", forecast_type) + def _condition_conversion(self, identifier, forecast_dt): """Convert from IPMA weather_type id to HA.""" if identifier == 1 and not is_up(self.hass, forecast_dt): @@ -135,10 +144,12 @@ class IPMAWeather(WeatherEntity, IPMADevice): @property def condition(self): """Return the current condition.""" - if not self._forecast: + forecast = self._hourly_forecast or self._daily_forecast + + if not forecast: return - return self._condition_conversion(self._forecast[0].weather_type.id, None) + return self._condition_conversion(forecast[0].weather_type.id, None) @property def native_temperature(self): @@ -180,10 +191,9 @@ class IPMAWeather(WeatherEntity, IPMADevice): return self._observation.wind_direction - @property - def forecast(self): + def _forecast(self, forecast: list[IPMAForecast] | None) -> list[Forecast]: """Return the forecast array.""" - if not self._forecast: + if not forecast: return [] return [ @@ -198,5 +208,32 @@ class IPMAWeather(WeatherEntity, IPMADevice): ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } - for data_in in self._forecast + for data_in in forecast ] + + @property + def forecast(self) -> list[Forecast]: + """Return the forecast array.""" + return self._forecast( + self._hourly_forecast if self._period == 1 else self._daily_forecast + ) + + async def _try_update_forecast( + self, + forecast_type: Literal["daily", "hourly"], + period: int, + ) -> None: + """Try to update weather forecast.""" + with contextlib.suppress(asyncio.TimeoutError): + async with async_timeout.timeout(10): + await self._update_forecast(forecast_type, period, False) + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + await self._try_update_forecast("daily", 24) + return self._forecast(self._daily_forecast) + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast in native units.""" + await self._try_update_forecast("hourly", 1) + return self._forecast(self._hourly_forecast) diff --git a/tests/components/ipma/snapshots/test_weather.ambr b/tests/components/ipma/snapshots/test_weather.ambr new file mode 100644 index 00000000000..92e1d1a91b5 --- /dev/null +++ b/tests/components/ipma/snapshots/test_weather.ambr @@ -0,0 +1,104 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'rainy', + 'datetime': datetime.datetime(2020, 1, 16, 0, 0), + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]), + }) +# --- +# name: test_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'rainy', + 'datetime': datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]), + }) +# --- +# name: test_forecast_subscription[daily] + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-16T00:00:00', + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]) +# --- +# name: test_forecast_subscription[daily].1 + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-16T00:00:00', + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-15T01:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2020-01-15T02:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-15T01:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2020-01-15T02:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]) +# --- diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index f9d69ec41ae..5bb1d8b8364 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,15 +1,9 @@ """Tests for IPMA config flow.""" from unittest.mock import Mock, patch -from homeassistant.components.ipma import DOMAIN, config_flow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE +from homeassistant.components.ipma import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component - -from . import MockLocation - -from tests.common import MockConfigEntry, mock_registry async def test_show_config_form() -> None: @@ -120,53 +114,3 @@ async def test_flow_entry_config_entry_already_exists() -> None: assert len(config_form.mock_calls) == 1 assert len(config_entries.mock_calls) == 1 assert len(flow._errors) == 1 - - -async def test_config_entry_migration(hass: HomeAssistant) -> None: - """Tests config entry without mode in unique_id can be migrated.""" - ipma_entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "daily"}, - ) - ipma_entry.add_to_hass(hass) - - ipma_entry2 = MockConfigEntry( - domain=DOMAIN, - title="Home", - data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "hourly"}, - ) - ipma_entry2.add_to_hass(hass) - - mock_registry( - hass, - { - "weather.hometown": er.RegistryEntry( - entity_id="weather.hometown", - unique_id="0, 0", - platform="ipma", - config_entry_id=ipma_entry.entry_id, - ), - "weather.hometown_2": er.RegistryEntry( - entity_id="weather.hometown_2", - unique_id="0, 0, hourly", - platform="ipma", - config_entry_id=ipma_entry.entry_id, - ), - }, - ) - - with patch( - "pyipma.location.Location.get", - return_value=MockLocation(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - ent_reg = er.async_get(hass) - - weather_home = ent_reg.async_get("weather.hometown") - assert weather_home.unique_id == "0, 0, daily" - - weather_home2 = ent_reg.async_get("weather.hometown_2") - assert weather_home2.unique_id == "0, 0, hourly" diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 285f7ceacb7..71884e0c82e 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,9 +1,12 @@ """The tests for the IPMA weather component.""" -from datetime import datetime +import datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.ipma.const import MIN_TIME_BETWEEN_UPDATES from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -18,6 +21,8 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -25,6 +30,7 @@ from homeassistant.core import HomeAssistant from . import MockLocation from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator TEST_CONFIG = { "name": "HomeTown", @@ -91,7 +97,7 @@ async def test_daily_forecast(hass: HomeAssistant) -> None: assert state.state == "rainy" forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_TIME) == datetime(2020, 1, 16, 0, 0, 0) + assert forecast.get(ATTR_FORECAST_TIME) == datetime.datetime(2020, 1, 16, 0, 0, 0) assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 @@ -144,3 +150,93 @@ async def test_failed_get_observation_forecast(hass: HomeAssistant) -> None: assert data.get(ATTR_WEATHER_WIND_SPEED) is None assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert state.attributes.get("friendly_name") == "HomeTown" + + +async def test_forecast_service( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.hometown", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.hometown", + "type": "hourly", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + +@pytest.mark.parametrize("forecast_type", ["daily", "hourly"]) +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": "weather.hometown", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert forecast1 == snapshot + + freezer.tick(MIN_TIME_BETWEEN_UPDATES + datetime.timedelta(seconds=1)) + await hass.async_block_till_done() + msg = await client.receive_json() + + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast2 = msg["event"]["forecast"] + + assert forecast2 == snapshot From 87753bdb825d3da601736fe46e4ad3d16c4b972a Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 12 Aug 2023 09:12:59 -0700 Subject: [PATCH 07/13] Add UniFi power stats for PDU overall AC outlet metrics (#98217) --- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi/sensor.py | 48 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_sensor.py | 91 ++++++++++++++++---- 5 files changed, 126 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 3b1fa68638b..8f27263b288 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==52"], + "requirements": ["aiounifi==53"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 367ff1332f4..142bd587853 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -12,11 +12,13 @@ from typing import Generic from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients +from aiounifi.interfaces.devices import Devices from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client +from aiounifi.models.device import Device from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.wlan import Wlan @@ -96,6 +98,12 @@ def async_device_outlet_power_supported_fn( return controller.api.outlets[obj_id].caps == 3 +@callback +def async_device_outlet_supported_fn(controller: UniFiController, obj_id: str) -> bool: + """Determine if a device supports reading overall power metrics.""" + return controller.api.devices[obj_id].outlet_ac_power_budget is not None + + @dataclass class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" @@ -224,6 +232,46 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}", value_fn=lambda _, obj: obj.power if obj.relay_state else "0", ), + UnifiSensorEntityDescription[Devices, Device]( + key="SmartPower AC power budget", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=1, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda device: "AC Power Budget", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=async_device_outlet_supported_fn, + unique_id_fn=lambda controller, obj_id: f"ac_power_budget-{obj_id}", + value_fn=lambda controller, device: device.outlet_ac_power_budget, + ), + UnifiSensorEntityDescription[Devices, Device]( + key="SmartPower AC power consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=1, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda device: "AC Power Consumption", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=async_device_outlet_supported_fn, + unique_id_fn=lambda controller, obj_id: f"ac_power_conumption-{obj_id}", + value_fn=lambda controller, device: device.outlet_ac_power_consumption, + ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 8eae7bc175f..aa512d9ffd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==52 +aiounifi==53 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7048126d3a8..3818300ea82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==52 +aiounifi==53 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 98a4941caaa..359825514d7 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -278,6 +278,27 @@ PDU_DEVICE_1 = { "x_has_ssh_hostkey": True, } +PDU_OUTLETS_UPDATE_DATA = [ + { + "index": 1, + "relay_state": True, + "cycle_enabled": False, + "name": "USB Outlet 1", + "outlet_caps": 1, + }, + { + "index": 2, + "relay_state": True, + "cycle_enabled": False, + "name": "Outlet 2", + "outlet_caps": 3, + "outlet_voltage": "119.644", + "outlet_current": "0.935", + "outlet_power": "123.45", + "outlet_power_factor": "0.659", + }, +] + async def test_no_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -719,31 +740,69 @@ async def test_wlan_client_sensors( assert hass.states.get("sensor.ssid_1").state == "0" +@pytest.mark.parametrize( + ( + "entity_id", + "expected_unique_id", + "expected_value", + "changed_data", + "expected_update_value", + ), + [ + ( + "dummy_usp_pdu_pro_outlet_2_outlet_power", + "outlet_power-01:02:03:04:05:ff_2", + "73.827", + {"outlet_table": PDU_OUTLETS_UPDATE_DATA}, + "123.45", + ), + ( + "dummy_usp_pdu_pro_ac_power_budget", + "ac_power_budget-01:02:03:04:05:ff", + "1875.000", + None, + None, + ), + ( + "dummy_usp_pdu_pro_ac_power_consumption", + "ac_power_conumption-01:02:03:04:05:ff", + "201.683", + {"outlet_ac_power_consumption": "456.78"}, + "456.78", + ), + ], +) async def test_outlet_power_readings( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, + entity_id: str, + expected_unique_id: str, + expected_value: any, + changed_data: dict | None, + expected_update_value: any, ) -> None: """Test the outlet power reporting on PDU devices.""" await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) - assert len(hass.states.async_all()) == 5 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 ent_reg = er.async_get(hass) - ent_reg_entry = ent_reg.async_get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") - assert ent_reg_entry.unique_id == "outlet_power-01:02:03:04:05:ff_2" + ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}") + assert ent_reg_entry.unique_id == expected_unique_id assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") - assert outlet_2.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert outlet_2.state == "73.827" + sensor_data = hass.states.get(f"sensor.{entity_id}") + assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert sensor_data.state == expected_value - # Verify state update - pdu_device_state_update = deepcopy(PDU_DEVICE_1) + if changed_data is not None: + updated_device_data = deepcopy(PDU_DEVICE_1) + updated_device_data.update(changed_data) - pdu_device_state_update["outlet_table"][1]["outlet_power"] = "123.45" + mock_unifi_websocket(message=MessageKey.DEVICE, data=updated_device_data) + await hass.async_block_till_done() - mock_unifi_websocket(message=MessageKey.DEVICE, data=pdu_device_state_update) - await hass.async_block_till_done() - - outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") - assert outlet_2.state == "123.45" + sensor_data = hass.states.get(f"sensor.{entity_id}") + assert sensor_data.state == expected_update_value From 4780ea6a5b8feb38bd43d6e3dd6c4030d3768484 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 12 Aug 2023 12:31:14 -0400 Subject: [PATCH 08/13] Bump Python-Roborock to 0.32.3 (#98303) bump to 0.32.3 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 05fff332c67..01548a6334c 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.32.2"] + "requirements": ["python-roborock==0.32.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa512d9ffd5..c03678db911 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2156,7 +2156,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.32.2 +python-roborock==0.32.3 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3818300ea82..37ddf1c45ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.32.2 +python-roborock==0.32.3 # homeassistant.components.smarttub python-smarttub==0.0.33 From 836b2de86f8e358dcfadc1c688631df36dbbc885 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:36:03 +0200 Subject: [PATCH 09/13] Add dataclass for Minecraft Server data (#98297) * Add dataclass for Minecraft server data * Sort dataclass variables --- .../components/minecraft_server/__init__.py | 54 +++++++++++-------- .../components/minecraft_server/entity.py | 4 +- .../components/minecraft_server/sensor.py | 16 +++--- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 6457f19a335..cf0d96af8d2 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any @@ -62,6 +63,19 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +@dataclass +class MinecraftServerData: + """Representation of Minecraft server data.""" + + latency: float | None = None + motd: str | None = None + players_max: int | None = None + players_online: int | None = None + players_list: list[str] | None = None + protocol_version: int | None = None + version: str | None = None + + class MinecraftServer: """Representation of a Minecraft server.""" @@ -84,13 +98,7 @@ class MinecraftServer: self._server = JavaServer(self.host, self.port) # Data provided by 3rd party library - self.version: str | None = None - self.protocol_version: int | None = None - self.latency: float | None = None - self.players_online: int | None = None - self.players_max: int | None = None - self.players_list: list[str] | None = None - self.motd: str | None = None + self.data: MinecraftServerData = MinecraftServerData() # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" @@ -170,18 +178,18 @@ class MinecraftServer: status_response = await self._server.async_status() # Got answer to request, update properties. - self.version = status_response.version.name - self.protocol_version = status_response.version.protocol - self.players_online = status_response.players.online - self.players_max = status_response.players.max - self.latency = status_response.latency - self.motd = status_response.motd.to_plain() + self.data.version = status_response.version.name + self.data.protocol_version = status_response.version.protocol + self.data.players_online = status_response.players.online + self.data.players_max = status_response.players.max + self.data.latency = status_response.latency + self.data.motd = status_response.motd.to_plain() - self.players_list = [] + self.data.players_list = [] if status_response.players.sample is not None: for player in status_response.players.sample: - self.players_list.append(player.name) - self.players_list.sort() + self.data.players_list.append(player.name) + self.data.players_list.sort() # Inform user once about successful update if necessary. if self._last_status_request_failed: @@ -193,13 +201,13 @@ class MinecraftServer: self._last_status_request_failed = False except OSError as error: # No answer to request, set all properties to unknown. - self.version = None - self.protocol_version = None - self.players_online = None - self.players_max = None - self.latency = None - self.players_list = None - self.motd = None + self.data.version = None + self.data.protocol_version = None + self.data.players_online = None + self.data.players_max = None + self.data.latency = None + self.data.players_list = None + self.data.motd = None # Inform user once about failed update if necessary. if not self._last_status_request_failed: diff --git a/homeassistant/components/minecraft_server/entity.py b/homeassistant/components/minecraft_server/entity.py index 9458a3ef397..63d68d0aa77 100644 --- a/homeassistant/components/minecraft_server/entity.py +++ b/homeassistant/components/minecraft_server/entity.py @@ -29,9 +29,9 @@ class MinecraftServerEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._server.unique_id)}, manufacturer=MANUFACTURER, - model=f"Minecraft Server ({self._server.version})", + model=f"Minecraft Server ({self._server.data.version})", name=self._server.name, - sw_version=str(self._server.protocol_version), + sw_version=f"{self._server.data.protocol_version}", ) self._attr_device_class = device_class self._extra_state_attributes = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 045aa3cec4e..74422675718 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -89,7 +89,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update version.""" - self._attr_native_value = self._server.version + self._attr_native_value = self._server.data.version class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): @@ -107,7 +107,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update protocol version.""" - self._attr_native_value = self._server.protocol_version + self._attr_native_value = self._server.data.protocol_version class MinecraftServerLatencySensor(MinecraftServerSensorEntity): @@ -126,7 +126,7 @@ class MinecraftServerLatencySensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update latency.""" - self._attr_native_value = self._server.latency + self._attr_native_value = self._server.data.latency class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): @@ -145,13 +145,13 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update online players state and device state attributes.""" - self._attr_native_value = self._server.players_online + self._attr_native_value = self._server.data.players_online extra_state_attributes = {} - players_list = self._server.players_list + players_list = self._server.data.players_list if players_list is not None and len(players_list) != 0: - extra_state_attributes[ATTR_PLAYERS_LIST] = self._server.players_list + extra_state_attributes[ATTR_PLAYERS_LIST] = players_list self._attr_extra_state_attributes = extra_state_attributes @@ -172,7 +172,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" - self._attr_native_value = self._server.players_max + self._attr_native_value = self._server.data.players_max class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): @@ -190,4 +190,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update MOTD.""" - self._attr_native_value = self._server.motd + self._attr_native_value = self._server.data.motd From 85b097af5076194a802f1e34dff199f2dc5f6114 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:39:59 +0200 Subject: [PATCH 10/13] Update todoist-api-python to 2.1.1 (#98263) --- homeassistant/components/todoist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index ac7e899d8a1..22d3b19b6c9 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "iot_class": "cloud_polling", "loggers": ["todoist"], - "requirements": ["todoist-api-python==2.0.2"] + "requirements": ["todoist-api-python==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c03678db911..4061b7bd08e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.1 # homeassistant.components.tolo tololib==0.1.0b4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37ddf1c45ea..da5ca884f24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ thermopro-ble==0.4.5 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.1 # homeassistant.components.tolo tololib==0.1.0b4 From 79991c32dcf06aafc13d25789b3a9349c68a5eed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Aug 2023 11:37:43 -0700 Subject: [PATCH 11/13] Bump pyrainbird to 4.0.0 (#98271) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 986e89783d7..07a0bc0a5f6 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==3.0.0"] + "requirements": ["pyrainbird==4.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4061b7bd08e..4c7bccd4cdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,7 +1955,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==3.0.0 +pyrainbird==4.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da5ca884f24..2d944127237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1453,7 +1453,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==3.0.0 +pyrainbird==4.0.0 # homeassistant.components.risco pyrisco==0.5.7 From bdaa2285fcf96735f6247cdd3321d6a83db8e188 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 12 Aug 2023 13:20:01 -0700 Subject: [PATCH 12/13] Google Assistant SDK: Allow responses for send_text_command (#95966) google_assistant_sdk.send_text_command response --- .../google_assistant_sdk/__init__.py | 24 ++++++++++++++++--- .../google_assistant_sdk/helpers.py | 13 +++++++++- .../google_assistant_sdk/test_init.py | 22 ++++++++++------- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 4a294489c97..24b71dd0180 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -1,6 +1,8 @@ """Support for Google Assistant SDK.""" from __future__ import annotations +import dataclasses + import aiohttp from gassist_text import TextAssistant from google.oauth2.credentials import Credentials @@ -9,7 +11,12 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( @@ -101,19 +108,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_service(hass: HomeAssistant) -> None: """Add the services for Google Assistant SDK.""" - async def send_text_command(call: ServiceCall) -> None: + async def send_text_command(call: ServiceCall) -> ServiceResponse: """Send a text command to Google Assistant SDK.""" commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] media_players: list[str] | None = call.data.get( SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER ) - await async_send_text_commands(hass, commands, media_players) + command_response_list = await async_send_text_commands( + hass, commands, media_players + ) + if call.return_response: + return { + "responses": [ + dataclasses.asdict(command_response) + for command_response in command_response_list + ] + } + return None hass.services.async_register( DOMAIN, SERVICE_SEND_TEXT_COMMAND, send_text_command, schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, ) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 1d89e208ced..5ae39c98f3c 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,6 +1,7 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +from dataclasses import dataclass from http import HTTPStatus import logging from typing import Any @@ -48,9 +49,16 @@ DEFAULT_LANGUAGE_CODES = { } +@dataclass +class CommandResponse: + """Response from a single command to Google Assistant Service.""" + + text: str + + async def async_send_text_commands( hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None -) -> None: +) -> list[CommandResponse]: """Send text commands to Google Assistant Service.""" # There can only be 1 entry (config_flow has single_instance_allowed) entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] @@ -68,6 +76,7 @@ async def async_send_text_commands( with TextAssistant( credentials, language_code, audio_out=bool(media_players) ) as assistant: + command_response_list = [] for command in commands: resp = assistant.assist(command) text_response = resp[0] @@ -91,6 +100,8 @@ async def async_send_text_commands( }, blocking=True, ) + command_response_list.append(CommandResponse(text_response)) + return command_response_list def default_language_code(hass: HomeAssistant): diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 3cb64a9a441..de89d562d46 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -162,20 +162,26 @@ async def test_send_text_commands( command1 = "open the garage door" command2 = "1234" + command1_response = "what's the PIN?" + command2_response = "opened the garage door" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" - ) as mock_text_assistant: - await hass.services.async_call( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=[ + (command1_response, None, None), + (command2_response, None, None), + ], + ) as mock_assist_call: + response = await hass.services.async_call( DOMAIN, "send_text_command", {"command": [command1, command2]}, blocking=True, + return_response=True, ) - mock_text_assistant.assert_called_once_with( - ExpectedCredentials(), "en-US", audio_out=False - ) - mock_text_assistant.assert_has_calls([call().__enter__().assist(command1)]) - mock_text_assistant.assert_has_calls([call().__enter__().assist(command2)]) + assert response == { + "responses": [{"text": command1_response}, {"text": command2_response}] + } + mock_assist_call.assert_has_calls([call(command1), call(command2)]) @pytest.mark.parametrize( From 483567529f9370dc76a5d58ac7dbde6bacd36672 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Aug 2023 20:10:36 -0500 Subject: [PATCH 13/13] Bump flux-led to 1.0.2 (#98312) changelog: https://github.com/Danielhiversen/flux_led/compare/1.0.1...1.0.2 fixes #98310 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 689f984722d..d3274738f75 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -54,5 +54,5 @@ "iot_class": "local_push", "loggers": ["flux_led"], "quality_scale": "platinum", - "requirements": ["flux-led==1.0.1"] + "requirements": ["flux-led==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4c7bccd4cdc..15c40edc32d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -800,7 +800,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.1 +flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d944127237..2e5e84347c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.1 +flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder