mirror of
https://github.com/home-assistant/core.git
synced 2025-08-07 06:35:10 +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, ...] = (
|
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=CURRENT_WARNING_SENSOR,
|
key=CURRENT_WARNING_SENSOR,
|
||||||
name="Current Warning Level",
|
translation_key=CURRENT_WARNING_SENSOR,
|
||||||
icon="mdi:close-octagon-outline",
|
icon="mdi:close-octagon-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=ADVANCE_WARNING_SENSOR,
|
key=ADVANCE_WARNING_SENSOR,
|
||||||
name="Advance Warning Level",
|
translation_key=ADVANCE_WARNING_SENSOR,
|
||||||
icon="mdi:close-octagon-outline",
|
icon="mdi:close-octagon-outline",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -131,6 +131,7 @@ class DwdWeatherWarningsSensor(
|
|||||||
"""Representation of a DWD-Weather-Warnings sensor."""
|
"""Representation of a DWD-Weather-Warnings sensor."""
|
||||||
|
|
||||||
_attr_attribution = "Data provided by DWD"
|
_attr_attribution = "Data provided by DWD"
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -142,7 +143,6 @@ class DwdWeatherWarningsSensor(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.entity_description = description
|
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_unique_id = f"{entry.unique_id}-{description.key}"
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
@@ -15,5 +15,15 @@
|
|||||||
"already_configured": "Warncell ID / name is already configured.",
|
"already_configured": "Warncell ID / name is already configured.",
|
||||||
"invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]"
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyenphase"],
|
"loggers": ["pyenphase"],
|
||||||
"requirements": ["pyenphase==1.4.0"],
|
"requirements": ["pyenphase==1.5.2"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_enphase-envoy._tcp.local."
|
"type": "_enphase-envoy._tcp.local."
|
||||||
|
@@ -8,6 +8,7 @@ import logging
|
|||||||
|
|
||||||
from pyenphase import (
|
from pyenphase import (
|
||||||
EnvoyEncharge,
|
EnvoyEncharge,
|
||||||
|
EnvoyEnchargeAggregate,
|
||||||
EnvoyEnchargePower,
|
EnvoyEnchargePower,
|
||||||
EnvoyEnpower,
|
EnvoyEnpower,
|
||||||
EnvoyInverter,
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@@ -329,6 +382,11 @@ async def async_setup_entry(
|
|||||||
for description in ENCHARGE_POWER_SENSORS
|
for description in ENCHARGE_POWER_SENSORS
|
||||||
for encharge in envoy_data.encharge_power
|
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:
|
if envoy_data.enpower:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
EnvoyEnpowerEntity(coordinator, description)
|
EnvoyEnpowerEntity(coordinator, description)
|
||||||
@@ -482,6 +540,19 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity):
|
|||||||
return self.entity_description.value_fn(encharge_power[self._serial_number])
|
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):
|
class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
|
||||||
"""Envoy Enpower sensor entity."""
|
"""Envoy Enpower sensor entity."""
|
||||||
|
|
||||||
|
@@ -63,6 +63,21 @@
|
|||||||
},
|
},
|
||||||
"lifetime_consumption": {
|
"lifetime_consumption": {
|
||||||
"name": "Lifetime energy 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": {
|
"switch": {
|
||||||
|
@@ -105,8 +105,8 @@ def esphome_state_property(
|
|||||||
if not self._has_state:
|
if not self._has_state:
|
||||||
return None
|
return None
|
||||||
val = func(self)
|
val = func(self)
|
||||||
if isinstance(val, float) and math.isnan(val):
|
if isinstance(val, float) and not math.isfinite(val):
|
||||||
# Home Assistant doesn't use NAN values in state machine
|
# Home Assistant doesn't use NaN or inf values in state machine
|
||||||
# (not JSON serializable)
|
# (not JSON serializable)
|
||||||
return None
|
return None
|
||||||
return val
|
return val
|
||||||
|
@@ -73,7 +73,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
|
|||||||
def native_value(self) -> float | None:
|
def native_value(self) -> float | None:
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
state = self._state
|
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 None
|
||||||
return state.state
|
return state.state
|
||||||
|
|
||||||
|
@@ -96,7 +96,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
|||||||
def native_value(self) -> datetime | str | None:
|
def native_value(self) -> datetime | str | None:
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
state = self._state
|
state = self._state
|
||||||
if math.isnan(state.state) or state.missing_state:
|
if state.missing_state or not math.isfinite(state.state):
|
||||||
return None
|
return None
|
||||||
if self._attr_device_class == SensorDeviceClass.TIMESTAMP:
|
if self._attr_device_class == SensorDeviceClass.TIMESTAMP:
|
||||||
return dt_util.utc_from_timestamp(state.state)
|
return dt_util.utc_from_timestamp(state.state)
|
||||||
|
@@ -54,5 +54,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["flux_led"],
|
"loggers": ["flux_led"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["flux-led==1.0.1"]
|
"requirements": ["flux-led==1.0.2"]
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
"""Support for Google Assistant SDK."""
|
"""Support for Google Assistant SDK."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from gassist_text import TextAssistant
|
from gassist_text import TextAssistant
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
@@ -9,7 +11,12 @@ import voluptuous as vol
|
|||||||
from homeassistant.components import conversation
|
from homeassistant.components import conversation
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
|
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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv, discovery, intent
|
from homeassistant.helpers import config_validation as cv, discovery, intent
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
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:
|
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||||
"""Add the services for Google Assistant SDK."""
|
"""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."""
|
"""Send a text command to Google Assistant SDK."""
|
||||||
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
||||||
media_players: list[str] | None = call.data.get(
|
media_players: list[str] | None = call.data.get(
|
||||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
|
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(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SEND_TEXT_COMMAND,
|
SERVICE_SEND_TEXT_COMMAND,
|
||||||
send_text_command,
|
send_text_command,
|
||||||
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
||||||
|
supports_response=SupportsResponse.OPTIONAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
"""Helper classes for Google Assistant SDK integration."""
|
"""Helper classes for Google Assistant SDK integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
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(
|
async def async_send_text_commands(
|
||||||
hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None
|
hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None
|
||||||
) -> None:
|
) -> list[CommandResponse]:
|
||||||
"""Send text commands to Google Assistant Service."""
|
"""Send text commands to Google Assistant Service."""
|
||||||
# There can only be 1 entry (config_flow has single_instance_allowed)
|
# There can only be 1 entry (config_flow has single_instance_allowed)
|
||||||
entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
@@ -68,6 +76,7 @@ async def async_send_text_commands(
|
|||||||
with TextAssistant(
|
with TextAssistant(
|
||||||
credentials, language_code, audio_out=bool(media_players)
|
credentials, language_code, audio_out=bool(media_players)
|
||||||
) as assistant:
|
) as assistant:
|
||||||
|
command_response_list = []
|
||||||
for command in commands:
|
for command in commands:
|
||||||
resp = assistant.assist(command)
|
resp = assistant.assist(command)
|
||||||
text_response = resp[0]
|
text_response = resp[0]
|
||||||
@@ -91,6 +100,8 @@ async def async_send_text_commands(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
command_response_list.append(CommandResponse(text_response))
|
||||||
|
return command_response_list
|
||||||
|
|
||||||
|
|
||||||
def default_language_code(hass: HomeAssistant):
|
def default_language_code(hass: HomeAssistant):
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
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
|
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):
|
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_NAME, default=name): str,
|
||||||
vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
|
vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
|
||||||
vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude,
|
vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude,
|
||||||
vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE),
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
errors=self._errors,
|
errors=self._errors,
|
||||||
|
@@ -49,6 +49,4 @@ CONDITION_CLASSES = {
|
|||||||
ATTR_CONDITION_CLEAR_NIGHT: [-1],
|
ATTR_CONDITION_CLEAR_NIGHT: [-1],
|
||||||
}
|
}
|
||||||
|
|
||||||
FORECAST_MODE = ["hourly", "daily"]
|
|
||||||
|
|
||||||
ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
|
ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
"""Support for IPMA weather service."""
|
"""Support for IPMA weather service."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from pyipma.api import IPMA_API
|
from pyipma.api import IPMA_API
|
||||||
from pyipma.forecast import Forecast
|
from pyipma.forecast import Forecast as IPMAForecast
|
||||||
from pyipma.location import Location
|
from pyipma.location import Location
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
@@ -16,7 +19,9 @@ from homeassistant.components.weather import (
|
|||||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||||
ATTR_FORECAST_TIME,
|
ATTR_FORECAST_TIME,
|
||||||
ATTR_FORECAST_WIND_BEARING,
|
ATTR_FORECAST_WIND_BEARING,
|
||||||
|
Forecast,
|
||||||
WeatherEntity,
|
WeatherEntity,
|
||||||
|
WeatherEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@@ -26,8 +31,7 @@ from homeassistant.const import (
|
|||||||
UnitOfSpeed,
|
UnitOfSpeed,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.sun import is_up
|
from homeassistant.helpers.sun import is_up
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
@@ -53,39 +57,19 @@ async def async_setup_entry(
|
|||||||
"""Add a weather entity from a config_entry."""
|
"""Add a weather entity from a config_entry."""
|
||||||
api = hass.data[DOMAIN][config_entry.entry_id][DATA_API]
|
api = hass.data[DOMAIN][config_entry.entry_id][DATA_API]
|
||||||
location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION]
|
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)
|
async_add_entities([IPMAWeather(location, api, config_entry.data)], True)
|
||||||
|
|
||||||
|
|
||||||
class IPMAWeather(WeatherEntity, IPMADevice):
|
class IPMAWeather(WeatherEntity, IPMADevice):
|
||||||
"""Representation of a weather condition."""
|
"""Representation of a weather condition."""
|
||||||
|
|
||||||
|
_attr_attribution = ATTRIBUTION
|
||||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||||
|
_attr_supported_features = (
|
||||||
_attr_attribution = ATTRIBUTION
|
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, location: Location, api: IPMA_API, config) -> None:
|
def __init__(self, location: Location, api: IPMA_API, config) -> None:
|
||||||
"""Initialise the platform with a data instance and station name."""
|
"""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._mode = config.get(CONF_MODE)
|
||||||
self._period = 1 if config.get(CONF_MODE) == "hourly" else 24
|
self._period = 1 if config.get(CONF_MODE) == "hourly" else 24
|
||||||
self._observation = None
|
self._observation = None
|
||||||
self._forecast: list[Forecast] = []
|
self._daily_forecast: list[IPMAForecast] | None = None
|
||||||
self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}"
|
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)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update Condition and Forecast."""
|
"""Update Condition and Forecast."""
|
||||||
async with async_timeout.timeout(10):
|
async with async_timeout.timeout(10):
|
||||||
new_observation = await self._location.observation(self._api)
|
new_observation = await self._location.observation(self._api)
|
||||||
new_forecast = await self._location.forecast(self._api, self._period)
|
|
||||||
|
|
||||||
if new_observation:
|
if new_observation:
|
||||||
self._observation = new_observation
|
self._observation = new_observation
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("Could not update weather observation")
|
_LOGGER.warning("Could not update weather observation")
|
||||||
|
|
||||||
if new_forecast:
|
if self._period == 24 or self._forecast_listeners["daily"]:
|
||||||
self._forecast = new_forecast
|
await self._update_forecast("daily", 24, True)
|
||||||
else:
|
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(
|
_LOGGER.debug(
|
||||||
"Updated location %s based on %s, current observation %s",
|
"Updated location %s based on %s, current observation %s",
|
||||||
@@ -122,6 +116,21 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
|||||||
self._observation,
|
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):
|
def _condition_conversion(self, identifier, forecast_dt):
|
||||||
"""Convert from IPMA weather_type id to HA."""
|
"""Convert from IPMA weather_type id to HA."""
|
||||||
if identifier == 1 and not is_up(self.hass, forecast_dt):
|
if identifier == 1 and not is_up(self.hass, forecast_dt):
|
||||||
@@ -135,10 +144,12 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
|||||||
@property
|
@property
|
||||||
def condition(self):
|
def condition(self):
|
||||||
"""Return the current condition."""
|
"""Return the current condition."""
|
||||||
if not self._forecast:
|
forecast = self._hourly_forecast or self._daily_forecast
|
||||||
|
|
||||||
|
if not forecast:
|
||||||
return
|
return
|
||||||
|
|
||||||
return self._condition_conversion(self._forecast[0].weather_type.id, None)
|
return self._condition_conversion(forecast[0].weather_type.id, None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_temperature(self):
|
def native_temperature(self):
|
||||||
@@ -180,10 +191,9 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
|||||||
|
|
||||||
return self._observation.wind_direction
|
return self._observation.wind_direction
|
||||||
|
|
||||||
@property
|
def _forecast(self, forecast: list[IPMAForecast] | None) -> list[Forecast]:
|
||||||
def forecast(self):
|
|
||||||
"""Return the forecast array."""
|
"""Return the forecast array."""
|
||||||
if not self._forecast:
|
if not forecast:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -198,5 +208,32 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
|||||||
ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength,
|
ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength,
|
||||||
ATTR_FORECAST_WIND_BEARING: data_in.wind_direction,
|
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 __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -62,6 +63,19 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
return unload_ok
|
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:
|
class MinecraftServer:
|
||||||
"""Representation of a Minecraft server."""
|
"""Representation of a Minecraft server."""
|
||||||
|
|
||||||
@@ -84,13 +98,7 @@ class MinecraftServer:
|
|||||||
self._server = JavaServer(self.host, self.port)
|
self._server = JavaServer(self.host, self.port)
|
||||||
|
|
||||||
# Data provided by 3rd party library
|
# Data provided by 3rd party library
|
||||||
self.version: str | None = None
|
self.data: MinecraftServerData = MinecraftServerData()
|
||||||
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
|
|
||||||
|
|
||||||
# Dispatcher signal name
|
# Dispatcher signal name
|
||||||
self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}"
|
self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}"
|
||||||
@@ -170,18 +178,18 @@ class MinecraftServer:
|
|||||||
status_response = await self._server.async_status()
|
status_response = await self._server.async_status()
|
||||||
|
|
||||||
# Got answer to request, update properties.
|
# Got answer to request, update properties.
|
||||||
self.version = status_response.version.name
|
self.data.version = status_response.version.name
|
||||||
self.protocol_version = status_response.version.protocol
|
self.data.protocol_version = status_response.version.protocol
|
||||||
self.players_online = status_response.players.online
|
self.data.players_online = status_response.players.online
|
||||||
self.players_max = status_response.players.max
|
self.data.players_max = status_response.players.max
|
||||||
self.latency = status_response.latency
|
self.data.latency = status_response.latency
|
||||||
self.motd = status_response.motd.to_plain()
|
self.data.motd = status_response.motd.to_plain()
|
||||||
|
|
||||||
self.players_list = []
|
self.data.players_list = []
|
||||||
if status_response.players.sample is not None:
|
if status_response.players.sample is not None:
|
||||||
for player in status_response.players.sample:
|
for player in status_response.players.sample:
|
||||||
self.players_list.append(player.name)
|
self.data.players_list.append(player.name)
|
||||||
self.players_list.sort()
|
self.data.players_list.sort()
|
||||||
|
|
||||||
# Inform user once about successful update if necessary.
|
# Inform user once about successful update if necessary.
|
||||||
if self._last_status_request_failed:
|
if self._last_status_request_failed:
|
||||||
@@ -193,13 +201,13 @@ class MinecraftServer:
|
|||||||
self._last_status_request_failed = False
|
self._last_status_request_failed = False
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
# No answer to request, set all properties to unknown.
|
# No answer to request, set all properties to unknown.
|
||||||
self.version = None
|
self.data.version = None
|
||||||
self.protocol_version = None
|
self.data.protocol_version = None
|
||||||
self.players_online = None
|
self.data.players_online = None
|
||||||
self.players_max = None
|
self.data.players_max = None
|
||||||
self.latency = None
|
self.data.latency = None
|
||||||
self.players_list = None
|
self.data.players_list = None
|
||||||
self.motd = None
|
self.data.motd = None
|
||||||
|
|
||||||
# Inform user once about failed update if necessary.
|
# Inform user once about failed update if necessary.
|
||||||
if not self._last_status_request_failed:
|
if not self._last_status_request_failed:
|
||||||
|
@@ -29,9 +29,9 @@ class MinecraftServerEntity(Entity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self._server.unique_id)},
|
identifiers={(DOMAIN, self._server.unique_id)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=f"Minecraft Server ({self._server.version})",
|
model=f"Minecraft Server ({self._server.data.version})",
|
||||||
name=self._server.name,
|
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._attr_device_class = device_class
|
||||||
self._extra_state_attributes = None
|
self._extra_state_attributes = None
|
||||||
|
@@ -89,7 +89,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update version."""
|
"""Update version."""
|
||||||
self._attr_native_value = self._server.version
|
self._attr_native_value = self._server.data.version
|
||||||
|
|
||||||
|
|
||||||
class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity):
|
class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity):
|
||||||
@@ -107,7 +107,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update protocol version."""
|
"""Update protocol version."""
|
||||||
self._attr_native_value = self._server.protocol_version
|
self._attr_native_value = self._server.data.protocol_version
|
||||||
|
|
||||||
|
|
||||||
class MinecraftServerLatencySensor(MinecraftServerSensorEntity):
|
class MinecraftServerLatencySensor(MinecraftServerSensorEntity):
|
||||||
@@ -126,7 +126,7 @@ class MinecraftServerLatencySensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update latency."""
|
"""Update latency."""
|
||||||
self._attr_native_value = self._server.latency
|
self._attr_native_value = self._server.data.latency
|
||||||
|
|
||||||
|
|
||||||
class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity):
|
class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity):
|
||||||
@@ -145,13 +145,13 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update online players state and device state attributes."""
|
"""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 = {}
|
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:
|
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
|
self._attr_extra_state_attributes = extra_state_attributes
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update maximum number of players."""
|
"""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):
|
class MinecraftServerMOTDSensor(MinecraftServerSensorEntity):
|
||||||
@@ -190,4 +190,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update MOTD."""
|
"""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."""
|
"""Plugwise Climate component for Home Assistant."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
@@ -145,14 +144,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
|||||||
"""Return the current preset mode."""
|
"""Return the current preset mode."""
|
||||||
return self.device.get("active_preset")
|
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
|
@plugwise_command
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/rainbird",
|
"documentation": "https://www.home-assistant.io/integrations/rainbird",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyrainbird"],
|
"loggers": ["pyrainbird"],
|
||||||
"requirements": ["pyrainbird==3.0.0"]
|
"requirements": ["pyrainbird==4.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["roborock"],
|
"loggers": ["roborock"],
|
||||||
"requirements": ["python-roborock==0.32.2"]
|
"requirements": ["python-roborock==0.32.3"]
|
||||||
}
|
}
|
||||||
|
@@ -327,7 +327,7 @@ class TadoConnector:
|
|||||||
device_type,
|
device_type,
|
||||||
"ON",
|
"ON",
|
||||||
mode,
|
mode,
|
||||||
fanSpeed=fan_speed,
|
fan_speed=fan_speed,
|
||||||
swing=swing,
|
swing=swing,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/todoist",
|
"documentation": "https://www.home-assistant.io/integrations/todoist",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["todoist"],
|
"loggers": ["todoist"],
|
||||||
"requirements": ["todoist-api-python==2.0.2"]
|
"requirements": ["todoist-api-python==2.1.1"]
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiounifi"],
|
"loggers": ["aiounifi"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiounifi==52"],
|
"requirements": ["aiounifi==53"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Ubiquiti Networks",
|
"manufacturer": "Ubiquiti Networks",
|
||||||
|
@@ -12,11 +12,13 @@ from typing import Generic
|
|||||||
|
|
||||||
from aiounifi.interfaces.api_handlers import ItemEvent
|
from aiounifi.interfaces.api_handlers import ItemEvent
|
||||||
from aiounifi.interfaces.clients import Clients
|
from aiounifi.interfaces.clients import Clients
|
||||||
|
from aiounifi.interfaces.devices import Devices
|
||||||
from aiounifi.interfaces.outlets import Outlets
|
from aiounifi.interfaces.outlets import Outlets
|
||||||
from aiounifi.interfaces.ports import Ports
|
from aiounifi.interfaces.ports import Ports
|
||||||
from aiounifi.interfaces.wlans import Wlans
|
from aiounifi.interfaces.wlans import Wlans
|
||||||
from aiounifi.models.api import ApiItemT
|
from aiounifi.models.api import ApiItemT
|
||||||
from aiounifi.models.client import Client
|
from aiounifi.models.client import Client
|
||||||
|
from aiounifi.models.device import Device
|
||||||
from aiounifi.models.outlet import Outlet
|
from aiounifi.models.outlet import Outlet
|
||||||
from aiounifi.models.port import Port
|
from aiounifi.models.port import Port
|
||||||
from aiounifi.models.wlan import Wlan
|
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
|
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
|
@dataclass
|
||||||
class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
|
class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
|
||||||
"""Validate and load entities from different UniFi handlers."""
|
"""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}",
|
unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}",
|
||||||
value_fn=lambda _, obj: obj.power if obj.relay_state else "0",
|
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
|
aiotractive==0.5.5
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==52
|
aiounifi==53
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
@@ -800,7 +800,7 @@ fjaraskupan==2.2.0
|
|||||||
flipr-api==1.5.0
|
flipr-api==1.5.0
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux-led==1.0.1
|
flux-led==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
# homeassistant.components.recorder
|
# homeassistant.components.recorder
|
||||||
@@ -1665,7 +1665,7 @@ pyedimax==0.2.1
|
|||||||
pyefergy==22.1.1
|
pyefergy==22.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==1.4.0
|
pyenphase==1.5.2
|
||||||
|
|
||||||
# homeassistant.components.envisalink
|
# homeassistant.components.envisalink
|
||||||
pyenvisalink==4.6
|
pyenvisalink==4.6
|
||||||
@@ -1955,7 +1955,7 @@ pyqwikswitch==0.93
|
|||||||
pyrail==0.0.3
|
pyrail==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.rainbird
|
# homeassistant.components.rainbird
|
||||||
pyrainbird==3.0.0
|
pyrainbird==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.recswitch
|
# homeassistant.components.recswitch
|
||||||
pyrecswitch==1.0.2
|
pyrecswitch==1.0.2
|
||||||
@@ -2156,7 +2156,7 @@ python-qbittorrent==0.4.3
|
|||||||
python-ripple-api==0.0.3
|
python-ripple-api==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.roborock
|
# homeassistant.components.roborock
|
||||||
python-roborock==0.32.2
|
python-roborock==0.32.3
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.33
|
python-smarttub==0.0.33
|
||||||
@@ -2554,7 +2554,7 @@ tilt-ble==0.2.3
|
|||||||
tmb==0.0.4
|
tmb==0.0.4
|
||||||
|
|
||||||
# homeassistant.components.todoist
|
# homeassistant.components.todoist
|
||||||
todoist-api-python==2.0.2
|
todoist-api-python==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.tolo
|
# homeassistant.components.tolo
|
||||||
tololib==0.1.0b4
|
tololib==0.1.0b4
|
||||||
|
@@ -335,7 +335,7 @@ aiosyncthing==0.5.1
|
|||||||
aiotractive==0.5.5
|
aiotractive==0.5.5
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==52
|
aiounifi==53
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
@@ -625,7 +625,7 @@ fjaraskupan==2.2.0
|
|||||||
flipr-api==1.5.0
|
flipr-api==1.5.0
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux-led==1.0.1
|
flux-led==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
# homeassistant.components.recorder
|
# homeassistant.components.recorder
|
||||||
@@ -1229,7 +1229,7 @@ pyeconet==0.1.20
|
|||||||
pyefergy==22.1.1
|
pyefergy==22.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==1.4.0
|
pyenphase==1.5.2
|
||||||
|
|
||||||
# homeassistant.components.everlights
|
# homeassistant.components.everlights
|
||||||
pyeverlights==0.1.0
|
pyeverlights==0.1.0
|
||||||
@@ -1453,7 +1453,7 @@ pyps4-2ndscreen==1.3.1
|
|||||||
pyqwikswitch==0.93
|
pyqwikswitch==0.93
|
||||||
|
|
||||||
# homeassistant.components.rainbird
|
# homeassistant.components.rainbird
|
||||||
pyrainbird==3.0.0
|
pyrainbird==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.risco
|
# homeassistant.components.risco
|
||||||
pyrisco==0.5.7
|
pyrisco==0.5.7
|
||||||
@@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0
|
|||||||
python-qbittorrent==0.4.3
|
python-qbittorrent==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.roborock
|
# homeassistant.components.roborock
|
||||||
python-roborock==0.32.2
|
python-roborock==0.32.3
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.33
|
python-smarttub==0.0.33
|
||||||
@@ -1860,7 +1860,7 @@ thermopro-ble==0.4.5
|
|||||||
tilt-ble==0.2.3
|
tilt-ble==0.2.3
|
||||||
|
|
||||||
# homeassistant.components.todoist
|
# homeassistant.components.todoist
|
||||||
todoist-api-python==2.0.2
|
todoist-api-python==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.tolo
|
# homeassistant.components.tolo
|
||||||
tololib==0.1.0b4
|
tololib==0.1.0b4
|
||||||
|
@@ -162,20 +162,26 @@ async def test_send_text_commands(
|
|||||||
|
|
||||||
command1 = "open the garage door"
|
command1 = "open the garage door"
|
||||||
command2 = "1234"
|
command2 = "1234"
|
||||||
|
command1_response = "what's the PIN?"
|
||||||
|
command2_response = "opened the garage door"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant"
|
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
|
||||||
) as mock_text_assistant:
|
side_effect=[
|
||||||
await hass.services.async_call(
|
(command1_response, None, None),
|
||||||
|
(command2_response, None, None),
|
||||||
|
],
|
||||||
|
) as mock_assist_call:
|
||||||
|
response = await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"send_text_command",
|
"send_text_command",
|
||||||
{"command": [command1, command2]},
|
{"command": [command1, command2]},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
)
|
)
|
||||||
mock_text_assistant.assert_called_once_with(
|
assert response == {
|
||||||
ExpectedCredentials(), "en-US", audio_out=False
|
"responses": [{"text": command1_response}, {"text": command2_response}]
|
||||||
)
|
}
|
||||||
mock_text_assistant.assert_has_calls([call().__enter__().assist(command1)])
|
mock_assist_call.assert_has_calls([call(command1), call(command2)])
|
||||||
mock_text_assistant.assert_has_calls([call().__enter__().assist(command2)])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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."""
|
"""Tests for IPMA config flow."""
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from homeassistant.components.ipma import DOMAIN, config_flow
|
from homeassistant.components.ipma import config_flow
|
||||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
from homeassistant.core import HomeAssistant
|
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:
|
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_form.mock_calls) == 1
|
||||||
assert len(config_entries.mock_calls) == 1
|
assert len(config_entries.mock_calls) == 1
|
||||||
assert len(flow._errors) == 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."""
|
"""The tests for the IPMA weather component."""
|
||||||
from datetime import datetime
|
import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.ipma.const import MIN_TIME_BETWEEN_UPDATES
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_FORECAST,
|
ATTR_FORECAST,
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
@@ -18,6 +21,8 @@ from homeassistant.components.weather import (
|
|||||||
ATTR_WEATHER_TEMPERATURE,
|
ATTR_WEATHER_TEMPERATURE,
|
||||||
ATTR_WEATHER_WIND_BEARING,
|
ATTR_WEATHER_WIND_BEARING,
|
||||||
ATTR_WEATHER_WIND_SPEED,
|
ATTR_WEATHER_WIND_SPEED,
|
||||||
|
DOMAIN as WEATHER_DOMAIN,
|
||||||
|
SERVICE_GET_FORECAST,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -25,6 +30,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from . import MockLocation
|
from . import MockLocation
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
TEST_CONFIG = {
|
TEST_CONFIG = {
|
||||||
"name": "HomeTown",
|
"name": "HomeTown",
|
||||||
@@ -91,7 +97,7 @@ async def test_daily_forecast(hass: HomeAssistant) -> None:
|
|||||||
assert state.state == "rainy"
|
assert state.state == "rainy"
|
||||||
|
|
||||||
forecast = state.attributes.get(ATTR_FORECAST)[0]
|
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_CONDITION) == "rainy"
|
||||||
assert forecast.get(ATTR_FORECAST_TEMP) == 16.2
|
assert forecast.get(ATTR_FORECAST_TEMP) == 16.2
|
||||||
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6
|
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_SPEED) is None
|
||||||
assert data.get(ATTR_WEATHER_WIND_BEARING) is None
|
assert data.get(ATTR_WEATHER_WIND_BEARING) is None
|
||||||
assert state.attributes.get("friendly_name") == "HomeTown"
|
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,
|
"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(
|
async def test_no_clients(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
@@ -719,31 +740,69 @@ async def test_wlan_client_sensors(
|
|||||||
assert hass.states.get("sensor.ssid_1").state == "0"
|
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(
|
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:
|
) -> None:
|
||||||
"""Test the outlet power reporting on PDU devices."""
|
"""Test the outlet power reporting on PDU devices."""
|
||||||
await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1])
|
await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1])
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 5
|
assert len(hass.states.async_all()) == 7
|
||||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
|
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
||||||
|
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
ent_reg_entry = ent_reg.async_get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}")
|
||||||
assert ent_reg_entry.unique_id == "outlet_power-01:02:03:04:05:ff_2"
|
assert ent_reg_entry.unique_id == expected_unique_id
|
||||||
assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC
|
assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
sensor_data = hass.states.get(f"sensor.{entity_id}")
|
||||||
assert outlet_2.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
|
assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
|
||||||
assert outlet_2.state == "73.827"
|
assert sensor_data.state == expected_value
|
||||||
|
|
||||||
# Verify state update
|
if changed_data is not None:
|
||||||
pdu_device_state_update = deepcopy(PDU_DEVICE_1)
|
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)
|
sensor_data = hass.states.get(f"sensor.{entity_id}")
|
||||||
await hass.async_block_till_done()
|
assert sensor_data.state == expected_update_value
|
||||||
|
|
||||||
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
|
||||||
assert outlet_2.state == "123.45"
|
|
||||||
|
Reference in New Issue
Block a user