mirror of
https://github.com/home-assistant/core.git
synced 2025-08-06 14:15:12 +02:00
Merge branch 'dev' into feature/starlink-device-tracker
This commit is contained in:
@@ -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(
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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."
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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,
|
||||
|
@@ -49,6 +49,4 @@ CONDITION_CLASSES = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: [-1],
|
||||
}
|
||||
|
||||
FORECAST_MODE = ["hourly", "daily"]
|
||||
|
||||
ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -327,7 +327,7 @@ class TadoConnector:
|
||||
device_type,
|
||||
"ON",
|
||||
mode,
|
||||
fanSpeed=fan_speed,
|
||||
fan_speed=fan_speed,
|
||||
swing=swing,
|
||||
)
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==52"],
|
||||
"requirements": ["aiounifi==53"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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(
|
||||
|
104
tests/components/ipma/snapshots/test_weather.ambr
Normal file
104
tests/components/ipma/snapshots/test_weather.ambr
Normal file
@@ -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,
|
||||
}),
|
||||
])
|
||||
# ---
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user