mirror of
https://github.com/home-assistant/core.git
synced 2026-01-10 17:47:16 +01:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740209a81d | ||
|
|
3d66065fe6 | ||
|
|
68c83ea629 | ||
|
|
7fb879f191 | ||
|
|
79924fcc7a | ||
|
|
5fa14aae7d | ||
|
|
588fb283cc | ||
|
|
48267c2705 | ||
|
|
9ee6ae8f94 | ||
|
|
431fe5950c | ||
|
|
e8b7ddc966 | ||
|
|
2485aa772c | ||
|
|
10525c7aa7 | ||
|
|
0c83156ba4 | ||
|
|
0d4331829c | ||
|
|
276874c414 | ||
|
|
ca5368f01f | ||
|
|
d4a7cefa8d | ||
|
|
473d0af85d | ||
|
|
eb16ca847a | ||
|
|
8feb382ae5 | ||
|
|
18eeda0e03 | ||
|
|
caf5020bac |
@@ -52,18 +52,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
coordinates=(lat, lon), precip_type=config.get(CONF_PRECIP_TYPE)
|
||||
)
|
||||
|
||||
add_devices([ECCamera(radar_object, config.get(CONF_NAME))], True)
|
||||
add_devices(
|
||||
[ECCamera(radar_object, config.get(CONF_NAME), config[CONF_LOOP])], True
|
||||
)
|
||||
|
||||
|
||||
class ECCamera(Camera):
|
||||
"""Implementation of an Environment Canada radar camera."""
|
||||
|
||||
def __init__(self, radar_object, camera_name):
|
||||
def __init__(self, radar_object, camera_name, is_loop):
|
||||
"""Initialize the camera."""
|
||||
super().__init__()
|
||||
|
||||
self.radar_object = radar_object
|
||||
self.camera_name = camera_name
|
||||
self.is_loop = is_loop
|
||||
self.content_type = "image/gif"
|
||||
self.image = None
|
||||
self.timestamp = None
|
||||
@@ -90,7 +93,7 @@ class ECCamera(Camera):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update radar image."""
|
||||
if CONF_LOOP:
|
||||
if self.is_loop:
|
||||
self.image = self.radar_object.get_loop()
|
||||
else:
|
||||
self.image = self.radar_object.get_latest_frame()
|
||||
|
||||
@@ -235,10 +235,10 @@ class FibaroController:
|
||||
scenes = self._client.scenes.list()
|
||||
self._scene_map = {}
|
||||
for device in scenes:
|
||||
if "visible" in device and not device.visible:
|
||||
if "name" not in device or "id" not in device:
|
||||
continue
|
||||
device.fibaro_controller = self
|
||||
if device.roomID == 0:
|
||||
if "roomID" not in device or device.roomID == 0:
|
||||
room_name = "Unknown"
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
@@ -250,6 +250,7 @@ class FibaroController:
|
||||
device.unique_id_str = f"{self.hub_serial}.scene.{device.id}"
|
||||
self._scene_map[device.id] = device
|
||||
self.fibaro_devices["scene"].append(device)
|
||||
_LOGGER.debug("%s scene -> %s", device.ha_id, device)
|
||||
|
||||
def _read_devices(self):
|
||||
"""Read and process the device list."""
|
||||
@@ -259,8 +260,10 @@ class FibaroController:
|
||||
last_climate_parent = None
|
||||
for device in devices:
|
||||
try:
|
||||
if "name" not in device or "id" not in device:
|
||||
continue
|
||||
device.fibaro_controller = self
|
||||
if device.roomID == 0:
|
||||
if "roomID" not in device or device.roomID == 0:
|
||||
room_name = "Unknown"
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from meteofrance.client import MeteoFranceClient
|
||||
from meteofrance.helpers import is_valid_warning_department
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@@ -20,6 +21,7 @@ from .const import (
|
||||
COORDINATOR_RAIN,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -77,15 +79,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
|
||||
async def _async_update_data_forecast_forecast():
|
||||
"""Fetch data from API endpoint."""
|
||||
return await hass.async_add_job(client.get_forecast, latitude, longitude)
|
||||
return await hass.async_add_executor_job(
|
||||
client.get_forecast, latitude, longitude
|
||||
)
|
||||
|
||||
async def _async_update_data_rain():
|
||||
"""Fetch data from API endpoint."""
|
||||
return await hass.async_add_job(client.get_rain, latitude, longitude)
|
||||
return await hass.async_add_executor_job(client.get_rain, latitude, longitude)
|
||||
|
||||
async def _async_update_data_alert():
|
||||
"""Fetch data from API endpoint."""
|
||||
return await hass.async_add_job(
|
||||
return await hass.async_add_executor_job(
|
||||
client.get_warning_current_phenomenoms, department, 0, True
|
||||
)
|
||||
|
||||
@@ -128,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
_LOGGER.debug(
|
||||
"Department corresponding to %s is %s", entry.title, department,
|
||||
)
|
||||
if department:
|
||||
if is_valid_warning_department(department):
|
||||
if not hass.data[DOMAIN].get(department):
|
||||
coordinator_alert = DataUpdateCoordinator(
|
||||
hass,
|
||||
@@ -152,14 +156,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Weather alert not available: The city %s is not in France or Andorre.",
|
||||
"Weather alert not available: The city %s is not in metropolitan France or Andorre.",
|
||||
entry.title,
|
||||
)
|
||||
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
COORDINATOR_RAIN: coordinator_rain,
|
||||
COORDINATOR_ALERT: coordinator_alert,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
for platform in PLATFORMS:
|
||||
@@ -192,8 +199,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if len(hass.data[DOMAIN]) == 0:
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
@@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Init MeteoFranceFlowHandler."""
|
||||
self.places = []
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return MeteoFranceOptionsFlowHandler(config_entry)
|
||||
|
||||
async def _show_setup_form(self, user_input=None, errors=None):
|
||||
@callback
|
||||
def _show_setup_form(self, user_input=None, errors=None):
|
||||
"""Show the setup form to the user."""
|
||||
|
||||
if user_input is None:
|
||||
@@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input is None:
|
||||
return await self._show_setup_form(user_input, errors)
|
||||
return self._show_setup_form(user_input, errors)
|
||||
|
||||
city = user_input[CONF_CITY] # Might be a city name or a postal code
|
||||
latitude = user_input.get(CONF_LATITUDE)
|
||||
@@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if not latitude:
|
||||
client = MeteoFranceClient()
|
||||
places = await self.hass.async_add_executor_job(client.search_places, city)
|
||||
_LOGGER.debug("places search result: %s", places)
|
||||
if not places:
|
||||
self.places = await self.hass.async_add_executor_job(
|
||||
client.search_places, city
|
||||
)
|
||||
_LOGGER.debug("Places search result: %s", self.places)
|
||||
if not self.places:
|
||||
errors[CONF_CITY] = "empty"
|
||||
return await self._show_setup_form(user_input, errors)
|
||||
return self._show_setup_form(user_input, errors)
|
||||
|
||||
return await self.async_step_cities(places=places)
|
||||
return await self.async_step_cities()
|
||||
|
||||
# Check if already configured
|
||||
await self.async_set_unique_id(f"{latitude}, {longitude}")
|
||||
@@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Import a config entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_cities(self, user_input=None, places=None):
|
||||
async def async_step_cities(self, user_input=None):
|
||||
"""Step where the user choose the city from the API search results."""
|
||||
if places and len(places) > 1 and self.source != SOURCE_IMPORT:
|
||||
places_for_form = {}
|
||||
for place in places:
|
||||
places_for_form[_build_place_key(place)] = f"{place}"
|
||||
if not user_input:
|
||||
if len(self.places) > 1 and self.source != SOURCE_IMPORT:
|
||||
places_for_form = {}
|
||||
for place in self.places:
|
||||
places_for_form[_build_place_key(place)] = f"{place}"
|
||||
|
||||
return await self._show_cities_form(places_for_form)
|
||||
# for import and only 1 city in the search result
|
||||
if places and not user_input:
|
||||
user_input = {CONF_CITY: _build_place_key(places[0])}
|
||||
return self.async_show_form(
|
||||
step_id="cities",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CITY): vol.All(
|
||||
vol.Coerce(str), vol.In(places_for_form)
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
user_input = {CONF_CITY: _build_place_key(self.places[0])}
|
||||
|
||||
city_infos = user_input.get(CONF_CITY).split(";")
|
||||
city_infos = user_input[CONF_CITY].split(";")
|
||||
return await self.async_step_user(
|
||||
{
|
||||
CONF_CITY: city_infos[0],
|
||||
@@ -95,15 +110,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
async def _show_cities_form(self, cities):
|
||||
"""Show the form to choose the city."""
|
||||
return self.async_show_form(
|
||||
step_id="cities",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_CITY): vol.All(vol.Coerce(str), vol.In(cities))}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a option flow."""
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Meteo-France component constants."""
|
||||
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
PRESSURE_HPA,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
@@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"]
|
||||
COORDINATOR_FORECAST = "coordinator_forecast"
|
||||
COORDINATOR_RAIN = "coordinator_rain"
|
||||
COORDINATOR_ALERT = "coordinator_alert"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
ATTRIBUTION = "Data provided by Météo-France"
|
||||
|
||||
CONF_CITY = "city"
|
||||
@@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast"
|
||||
ENTITY_NAME = "name"
|
||||
ENTITY_UNIT = "unit"
|
||||
ENTITY_ICON = "icon"
|
||||
ENTITY_CLASS = "device_class"
|
||||
ENTITY_DEVICE_CLASS = "device_class"
|
||||
ENTITY_ENABLE = "enable"
|
||||
ENTITY_API_DATA_PATH = "data_path"
|
||||
|
||||
@@ -32,8 +36,8 @@ SENSOR_TYPES = {
|
||||
"pressure": {
|
||||
ENTITY_NAME: "Pressure",
|
||||
ENTITY_UNIT: PRESSURE_HPA,
|
||||
ENTITY_ICON: "mdi:gauge",
|
||||
ENTITY_CLASS: "pressure",
|
||||
ENTITY_ICON: None,
|
||||
ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
ENTITY_ENABLE: False,
|
||||
ENTITY_API_DATA_PATH: "current_forecast:sea_level",
|
||||
},
|
||||
@@ -41,7 +45,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Rain chance",
|
||||
ENTITY_UNIT: UNIT_PERCENTAGE,
|
||||
ENTITY_ICON: "mdi:weather-rainy",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "probability_forecast:rain:3h",
|
||||
},
|
||||
@@ -49,7 +53,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Snow chance",
|
||||
ENTITY_UNIT: UNIT_PERCENTAGE,
|
||||
ENTITY_ICON: "mdi:weather-snowy",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "probability_forecast:snow:3h",
|
||||
},
|
||||
@@ -57,7 +61,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Freeze chance",
|
||||
ENTITY_UNIT: UNIT_PERCENTAGE,
|
||||
ENTITY_ICON: "mdi:snowflake",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "probability_forecast:freezing",
|
||||
},
|
||||
@@ -65,23 +69,23 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Wind speed",
|
||||
ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
ENTITY_ICON: "mdi:weather-windy",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: False,
|
||||
ENTITY_API_DATA_PATH: "current_forecast:wind:speed",
|
||||
},
|
||||
"next_rain": {
|
||||
ENTITY_NAME: "Next rain",
|
||||
ENTITY_UNIT: None,
|
||||
ENTITY_ICON: "mdi:weather-pouring",
|
||||
ENTITY_CLASS: "timestamp",
|
||||
ENTITY_ICON: None,
|
||||
ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: None,
|
||||
},
|
||||
"temperature": {
|
||||
ENTITY_NAME: "Temperature",
|
||||
ENTITY_UNIT: TEMP_CELSIUS,
|
||||
ENTITY_ICON: "mdi:thermometer",
|
||||
ENTITY_CLASS: "temperature",
|
||||
ENTITY_ICON: None,
|
||||
ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_ENABLE: False,
|
||||
ENTITY_API_DATA_PATH: "current_forecast:T:value",
|
||||
},
|
||||
@@ -89,7 +93,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "UV",
|
||||
ENTITY_UNIT: None,
|
||||
ENTITY_ICON: "mdi:sunglasses",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "today_forecast:uv",
|
||||
},
|
||||
@@ -97,7 +101,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Weather alert",
|
||||
ENTITY_UNIT: None,
|
||||
ENTITY_ICON: "mdi:weather-cloudy-alert",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: None,
|
||||
},
|
||||
@@ -105,7 +109,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Daily precipitation",
|
||||
ENTITY_UNIT: "mm",
|
||||
ENTITY_ICON: "mdi:cup-water",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h",
|
||||
},
|
||||
@@ -113,7 +117,7 @@ SENSOR_TYPES = {
|
||||
ENTITY_NAME: "Cloud cover",
|
||||
ENTITY_UNIT: UNIT_PERCENTAGE,
|
||||
ENTITY_ICON: "mdi:weather-partly-cloudy",
|
||||
ENTITY_CLASS: None,
|
||||
ENTITY_DEVICE_CLASS: None,
|
||||
ENTITY_ENABLE: True,
|
||||
ENTITY_API_DATA_PATH: "current_forecast:clouds",
|
||||
},
|
||||
@@ -128,7 +132,7 @@ CONDITION_CLASSES = {
|
||||
"Brouillard",
|
||||
"Brouillard givrant",
|
||||
],
|
||||
"hail": ["Risque de grêle"],
|
||||
"hail": ["Risque de grêle", "Risque de grèle"],
|
||||
"lightning": ["Risque d'orages", "Orages"],
|
||||
"lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
|
||||
"partlycloudy": [
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
"name": "Météo-France",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
|
||||
"requirements": ["meteofrance-api==0.1.0"],
|
||||
"codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"]
|
||||
}
|
||||
"requirements": [
|
||||
"meteofrance-api==0.1.1"
|
||||
],
|
||||
"codeowners": [
|
||||
"@hacf-fr",
|
||||
"@oncleben31",
|
||||
"@Quentame"
|
||||
]
|
||||
}
|
||||
@@ -21,7 +21,7 @@ from .const import (
|
||||
COORDINATOR_RAIN,
|
||||
DOMAIN,
|
||||
ENTITY_API_DATA_PATH,
|
||||
ENTITY_CLASS,
|
||||
ENTITY_DEVICE_CLASS,
|
||||
ENTITY_ENABLE,
|
||||
ENTITY_ICON,
|
||||
ENTITY_NAME,
|
||||
@@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return SENSOR_TYPES[self._type][ENTITY_CLASS]
|
||||
return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS]
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
@@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
next_rain_date_locale = self.coordinator.data.next_rain_date_locale()
|
||||
# search first cadran with rain
|
||||
next_rain = next(
|
||||
(cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1),
|
||||
None,
|
||||
)
|
||||
return (
|
||||
dt_util.as_local(next_rain_date_locale) if next_rain_date_locale else None
|
||||
dt_util.utc_from_timestamp(next_rain["dt"]).isoformat()
|
||||
if next_rain
|
||||
else None
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_NEXT_RAIN_1_HOUR_FORECAST: [
|
||||
{
|
||||
dt_util.as_local(
|
||||
self.coordinator.data.timestamp_to_locale_time(item["dt"])
|
||||
).strftime("%H:%M"): item["desc"]
|
||||
}
|
||||
{dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]}
|
||||
for item in self.coordinator.data.forecast
|
||||
],
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MODE, TEMP_CELSIUS
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
@@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity):
|
||||
continue
|
||||
forecast_data.append(
|
||||
{
|
||||
ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time(
|
||||
ATTR_FORECAST_TIME: dt_util.utc_from_timestamp(
|
||||
forecast["dt"]
|
||||
),
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_CONDITION: format_condition(
|
||||
forecast["weather"]["desc"]
|
||||
),
|
||||
|
||||
@@ -262,8 +262,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
||||
for room in home["rooms"]:
|
||||
if data["event_type"] == "set_point":
|
||||
if self._id == room["id"]:
|
||||
if room["therm_setpoint_mode"] == "off":
|
||||
if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
|
||||
self._hvac_mode = HVAC_MODE_OFF
|
||||
elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX:
|
||||
self._hvac_mode = HVAC_MODE_HEAT
|
||||
self._target_temperature = DEFAULT_MAX_TEMP
|
||||
else:
|
||||
self._target_temperature = room["therm_setpoint_temperature"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -239,6 +239,7 @@ class OnkyoDevice(MediaPlayerEntity):
|
||||
self._source_mapping = sources
|
||||
self._reverse_mapping = {value: key for key, value in sources.items()}
|
||||
self._attributes = {}
|
||||
self._hdmi_out_supported = True
|
||||
|
||||
def command(self, command):
|
||||
"""Run an eiscp command and catch connection errors."""
|
||||
@@ -251,6 +252,7 @@ class OnkyoDevice(MediaPlayerEntity):
|
||||
else:
|
||||
_LOGGER.info("%s is disconnected. Attempting to reconnect", self._name)
|
||||
return False
|
||||
_LOGGER.debug("Result for %s: %s", command, result)
|
||||
return result
|
||||
|
||||
def update(self):
|
||||
@@ -268,7 +270,13 @@ class OnkyoDevice(MediaPlayerEntity):
|
||||
volume_raw = self.command("volume query")
|
||||
mute_raw = self.command("audio-muting query")
|
||||
current_source_raw = self.command("input-selector query")
|
||||
hdmi_out_raw = self.command("hdmi-output-selector query")
|
||||
# If the following command is sent to a device with only one HDMI out,
|
||||
# the display shows 'Not Available'.
|
||||
# We avoid this by checking if HDMI out is supported
|
||||
if self._hdmi_out_supported:
|
||||
hdmi_out_raw = self.command("hdmi-output-selector query")
|
||||
else:
|
||||
hdmi_out_raw = []
|
||||
preset_raw = self.command("preset query")
|
||||
if not (volume_raw and mute_raw and current_source_raw):
|
||||
return
|
||||
@@ -298,6 +306,8 @@ class OnkyoDevice(MediaPlayerEntity):
|
||||
if not hdmi_out_raw:
|
||||
return
|
||||
self._attributes["video_out"] = ",".join(hdmi_out_raw[1])
|
||||
if hdmi_out_raw[1] == "N/A":
|
||||
self._hdmi_out_supported = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -40,7 +40,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
"""Fetch data from OVO Energy."""
|
||||
now = datetime.utcnow()
|
||||
async with async_timeout.timeout(10):
|
||||
return await client.get_daily_usage(now.strftime("%Y-%m"))
|
||||
try:
|
||||
await client.authenticate(
|
||||
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
|
||||
)
|
||||
return await client.get_daily_usage(now.strftime("%Y-%m"))
|
||||
except aiohttp.ClientError as exception:
|
||||
_LOGGER.warning(exception)
|
||||
return None
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "OVO Energy",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ovo_energy",
|
||||
"requirements": ["ovoenergy==1.1.6"],
|
||||
"requirements": ["ovoenergy==1.1.7"],
|
||||
"codeowners": ["@timmo001"]
|
||||
}
|
||||
|
||||
@@ -27,18 +27,34 @@ async def async_setup_entry(
|
||||
]
|
||||
client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
|
||||
|
||||
currency = coordinator.data.electricity[
|
||||
len(coordinator.data.electricity) - 1
|
||||
].cost.currency_unit
|
||||
entities = []
|
||||
|
||||
if coordinator.data:
|
||||
if coordinator.data.electricity:
|
||||
entities.append(OVOEnergyLastElectricityReading(coordinator, client))
|
||||
entities.append(
|
||||
OVOEnergyLastElectricityCost(
|
||||
coordinator,
|
||||
client,
|
||||
coordinator.data.electricity[
|
||||
len(coordinator.data.electricity) - 1
|
||||
].cost.currency_unit,
|
||||
)
|
||||
)
|
||||
if coordinator.data.gas:
|
||||
entities.append(OVOEnergyLastGasReading(coordinator, client))
|
||||
entities.append(
|
||||
OVOEnergyLastGasCost(
|
||||
coordinator,
|
||||
client,
|
||||
coordinator.data.gas[
|
||||
len(coordinator.data.gas) - 1
|
||||
].cost.currency_unit,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
OVOEnergyLastElectricityReading(coordinator, client),
|
||||
OVOEnergyLastGasReading(coordinator, client),
|
||||
OVOEnergyLastElectricityCost(coordinator, client, currency),
|
||||
OVOEnergyLastGasCost(coordinator, client, currency),
|
||||
],
|
||||
True,
|
||||
entities, True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -80,24 +80,24 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
|
||||
if self.values.dimming_duration is not None:
|
||||
self._supported_features |= SUPPORT_TRANSITION
|
||||
|
||||
if self.values.color is None or self.values.color_channels is None:
|
||||
return
|
||||
if self.values.color is not None:
|
||||
self._supported_features |= SUPPORT_COLOR
|
||||
|
||||
self._supported_features |= SUPPORT_COLOR
|
||||
if self.values.color_channels is not None:
|
||||
# Support Color Temp if both white channels
|
||||
if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and (
|
||||
self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE
|
||||
):
|
||||
self._supported_features |= SUPPORT_COLOR_TEMP
|
||||
|
||||
# Support Color Temp if both white channels
|
||||
if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and (
|
||||
self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE
|
||||
):
|
||||
self._supported_features |= SUPPORT_COLOR_TEMP
|
||||
# Support White value if only a single white channel
|
||||
if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ (
|
||||
(self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0
|
||||
):
|
||||
self._supported_features |= SUPPORT_WHITE_VALUE
|
||||
|
||||
# Support White value if only a single white channel
|
||||
if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ (
|
||||
(self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0
|
||||
):
|
||||
self._supported_features |= SUPPORT_WHITE_VALUE
|
||||
|
||||
self._calculate_rgb_values()
|
||||
if self.values.color is not None:
|
||||
self._calculate_color_values()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@@ -172,7 +172,7 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
|
||||
|
||||
else:
|
||||
# transition specified by user
|
||||
new_value = max(0, min(7620, kwargs[ATTR_TRANSITION]))
|
||||
new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION])))
|
||||
if ozw_version < (1, 6, 1205):
|
||||
transition = kwargs[ATTR_TRANSITION]
|
||||
if transition <= 127:
|
||||
@@ -248,10 +248,8 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
|
||||
|
||||
self.values.primary.send_value(0)
|
||||
|
||||
def _calculate_rgb_values(self):
|
||||
# Color Channels
|
||||
self._color_channels = self.values.color_channels.data[ATTR_VALUE]
|
||||
|
||||
def _calculate_color_values(self):
|
||||
"""Parse color rgb and color temperature data."""
|
||||
# Color Data String
|
||||
data = self.values.color.data[ATTR_VALUE]
|
||||
|
||||
@@ -259,6 +257,12 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
|
||||
rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)]
|
||||
self._hs = color_util.color_RGB_to_hs(*rgb)
|
||||
|
||||
if self.values.color_channels is None:
|
||||
return
|
||||
|
||||
# Color Channels
|
||||
self._color_channels = self.values.color_channels.data[ATTR_VALUE]
|
||||
|
||||
# Parse remaining color channels. OpenZWave appends white channels
|
||||
# that are present.
|
||||
index = 7
|
||||
|
||||
@@ -534,7 +534,15 @@ class Recorder(threading.Thread):
|
||||
if self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith(
|
||||
SQLITE_URL_PREFIX
|
||||
):
|
||||
validate_or_move_away_sqlite_database(self.db_url)
|
||||
with self.hass.timeout.freeze(DOMAIN):
|
||||
#
|
||||
# Here we run an sqlite3 quick_check. In the majority
|
||||
# of cases, the quick_check takes under 10 seconds.
|
||||
#
|
||||
# On systems with very large databases and
|
||||
# very slow disk or cpus, this can take a while.
|
||||
#
|
||||
validate_or_move_away_sqlite_database(self.db_url)
|
||||
|
||||
if self.engine is not None:
|
||||
self.engine.dispose()
|
||||
|
||||
@@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
token=self.token,
|
||||
timeout=10,
|
||||
timeout=8,
|
||||
name=VALUE_CONF_NAME,
|
||||
)
|
||||
self._remote.open()
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
from uuid import UUID
|
||||
|
||||
from simplipy import API
|
||||
from simplipy.errors import InvalidCredentialsError, SimplipyError
|
||||
from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError
|
||||
from simplipy.websocket import (
|
||||
EVENT_CAMERA_MOTION_DETECTED,
|
||||
EVENT_CONNECTION_LOST,
|
||||
@@ -555,6 +555,13 @@ class SimpliSafe:
|
||||
LOGGER.error("Error while using stored refresh token: %s", err)
|
||||
return
|
||||
|
||||
if isinstance(result, EndpointUnavailable):
|
||||
# In case the user attempt an action not allowed in their current plan,
|
||||
# we merely log that message at INFO level (so the user is aware,
|
||||
# but not spammed with ERROR messages that they cannot change):
|
||||
LOGGER.info(result)
|
||||
return
|
||||
|
||||
if isinstance(result, SimplipyError):
|
||||
LOGGER.error("SimpliSafe error while updating: %s", result)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==9.2.2"],
|
||||
"requirements": ["simplisafe-python==9.3.0"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Open ports in your router for Home Assistant and provide statistics."""
|
||||
import asyncio
|
||||
from ipaddress import ip_address
|
||||
from operator import itemgetter
|
||||
|
||||
@@ -106,7 +107,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
|
||||
# discover and construct
|
||||
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
|
||||
st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name
|
||||
device = await async_discover_and_construct(hass, udn, st)
|
||||
try:
|
||||
device = await async_discover_and_construct(hass, udn, st)
|
||||
except asyncio.TimeoutError:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if not device:
|
||||
_LOGGER.info("Unable to create UPnP/IGD, aborting")
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Belkin WeMo",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wemo",
|
||||
"requirements": ["pywemo==0.4.45"],
|
||||
"requirements": ["pywemo==0.4.46"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Belkin International Inc."
|
||||
|
||||
@@ -8,6 +8,7 @@ import voluptuous as vol
|
||||
from zeroconf import (
|
||||
DNSPointer,
|
||||
DNSRecord,
|
||||
Error as ZeroconfError,
|
||||
InterfaceChoice,
|
||||
IPVersion,
|
||||
NonUniqueNameException,
|
||||
@@ -208,7 +209,12 @@ def setup(hass, config):
|
||||
if state_change != ServiceStateChange.Added:
|
||||
return
|
||||
|
||||
service_info = zeroconf.get_service_info(service_type, name)
|
||||
try:
|
||||
service_info = zeroconf.get_service_info(service_type, name)
|
||||
except ZeroconfError:
|
||||
_LOGGER.exception("Failed to get info for device %s", name)
|
||||
return
|
||||
|
||||
if not service_info:
|
||||
# Prevent the browser thread from collapsing as
|
||||
# service_info can be None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 114
|
||||
PATCH_VERSION = "0"
|
||||
PATCH_VERSION = "2"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 1)
|
||||
|
||||
@@ -23,8 +23,8 @@ if TYPE_CHECKING:
|
||||
|
||||
SLOW_SETUP_WARNING = 10
|
||||
SLOW_SETUP_MAX_WAIT = 60
|
||||
SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity
|
||||
SLOW_ADD_MIN_TIMEOUT = 60
|
||||
SLOW_ADD_ENTITY_MAX_WAIT = 15 # Per Entity
|
||||
SLOW_ADD_MIN_TIMEOUT = 500
|
||||
|
||||
PLATFORM_NOT_READY_RETRIES = 10
|
||||
DATA_ENTITY_PLATFORM = "entity_platform"
|
||||
|
||||
@@ -11,6 +11,18 @@ from homeassistant import bootstrap
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.frame import warn_use
|
||||
|
||||
#
|
||||
# Python 3.8 has significantly less workers by default
|
||||
# than Python 3.7. In order to be consistent between
|
||||
# supported versions, we need to set max_workers.
|
||||
#
|
||||
# In most cases the workers are not I/O bound, as they
|
||||
# are sleeping/blocking waiting for data from integrations
|
||||
# updating so this number should be higher than the default
|
||||
# use case.
|
||||
#
|
||||
MAX_EXECUTOR_WORKERS = 64
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RuntimeConfig:
|
||||
@@ -57,7 +69,9 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore
|
||||
if self.debug:
|
||||
loop.set_debug(True)
|
||||
|
||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||
executor = ThreadPoolExecutor(
|
||||
thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS
|
||||
)
|
||||
loop.set_default_executor(executor)
|
||||
loop.set_default_executor = warn_use( # type: ignore
|
||||
loop.set_default_executor, "sets default executor on the event loop"
|
||||
|
||||
@@ -902,7 +902,7 @@ messagebird==1.2.0
|
||||
meteoalertapi==0.1.6
|
||||
|
||||
# homeassistant.components.meteo_france
|
||||
meteofrance-api==0.1.0
|
||||
meteofrance-api==0.1.1
|
||||
|
||||
# homeassistant.components.mfi
|
||||
mficlient==0.3.0
|
||||
@@ -1029,7 +1029,7 @@ oru==0.1.11
|
||||
orvibo==1.1.1
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.1.6
|
||||
ovoenergy==1.1.7
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@@ -1837,7 +1837,7 @@ pyvolumio==0.1.1
|
||||
pywebpush==1.9.2
|
||||
|
||||
# homeassistant.components.wemo
|
||||
pywemo==0.4.45
|
||||
pywemo==0.4.46
|
||||
|
||||
# homeassistant.components.xeoma
|
||||
pyxeoma==1.4.1
|
||||
@@ -1963,7 +1963,7 @@ simplehound==0.3
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==9.2.2
|
||||
simplisafe-python==9.3.0
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==2.2.1
|
||||
|
||||
@@ -424,7 +424,7 @@ mbddns==0.1.2
|
||||
mcstatus==2.3.0
|
||||
|
||||
# homeassistant.components.meteo_france
|
||||
meteofrance-api==0.1.0
|
||||
meteofrance-api==0.1.1
|
||||
|
||||
# homeassistant.components.mfi
|
||||
mficlient==0.3.0
|
||||
@@ -473,7 +473,7 @@ onvif-zeep-async==0.4.0
|
||||
openerz-api==0.1.0
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.1.6
|
||||
ovoenergy==1.1.7
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@@ -878,7 +878,7 @@ sentry-sdk==0.13.5
|
||||
simplehound==0.3
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==9.2.2
|
||||
simplisafe-python==9.3.0
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
sleepyq==0.7
|
||||
|
||||
@@ -33,6 +33,12 @@ def light_new_ozw_data_fixture():
|
||||
return load_fixture("ozw/light_new_ozw_network_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="light_pure_rgb_dimmer_data", scope="session")
|
||||
def light_pure_rgb_dimmer_data_fixture():
|
||||
"""Load light rgb and dimmer MQTT data and return it."""
|
||||
return load_fixture("ozw/light_pure_rgb_dimmer_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="light_no_rgb_data", scope="session")
|
||||
def light_no_rgb_data_fixture():
|
||||
"""Load light dimmer MQTT data and return it."""
|
||||
@@ -139,6 +145,17 @@ async def light_rgb_msg_fixture(hass):
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture(name="light_pure_rgb_msg")
|
||||
async def light_pure_rgb_msg_fixture(hass):
|
||||
"""Return a mock MQTT msg with a pure rgb light actuator message."""
|
||||
light_json = json.loads(
|
||||
await hass.async_add_executor_job(load_fixture, "ozw/light_pure_rgb.json")
|
||||
)
|
||||
message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"])
|
||||
message.encode()
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture(name="switch_msg")
|
||||
async def switch_msg_fixture(hass):
|
||||
"""Return a mock MQTT msg with a switch actuator message."""
|
||||
|
||||
@@ -85,7 +85,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
|
||||
assert state.state == "off"
|
||||
|
||||
# Test turn on without brightness
|
||||
new_transition = 127
|
||||
new_transition = 127.0
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
@@ -350,6 +350,48 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
|
||||
assert state.attributes["color_temp"] == 153
|
||||
|
||||
|
||||
async def test_pure_rgb_dimmer_light(
|
||||
hass, light_pure_rgb_dimmer_data, light_msg, light_pure_rgb_msg, sent_messages
|
||||
):
|
||||
"""Test light with no color channels command class."""
|
||||
receive_message = await setup_ozw(hass, fixture=light_pure_rgb_dimmer_data)
|
||||
|
||||
# Test loaded
|
||||
state = hass.states.get("light.kitchen_rgb_strip_level")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
assert state.attributes["supported_features"] == 17
|
||||
|
||||
# Test setting hs_color
|
||||
new_color = [300, 70]
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": "light.kitchen_rgb_strip_level", "hs_color": new_color},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 2
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 255, "ValueIDKey": 122257425}
|
||||
|
||||
msg = sent_messages[-2]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": "#ff4cff0000", "ValueIDKey": 122470423}
|
||||
|
||||
# Feedback on state
|
||||
light_pure_rgb_msg.decode()
|
||||
light_pure_rgb_msg.payload["Value"] = "#ff4cff0000"
|
||||
light_pure_rgb_msg.encode()
|
||||
receive_message(light_pure_rgb_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.kitchen_rgb_strip_level")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
assert state.attributes["hs_color"] == (300.0, 70.196)
|
||||
|
||||
|
||||
async def test_no_rgb_light(hass, light_no_rgb_data, light_no_rgb_msg, sent_messages):
|
||||
"""Test setting up config entry."""
|
||||
receive_message = await setup_ozw(hass, fixture=light_no_rgb_data)
|
||||
|
||||
@@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = {
|
||||
"host": "fake",
|
||||
"name": "HomeAssistant",
|
||||
"port": 8001,
|
||||
"timeout": 10,
|
||||
"timeout": 8,
|
||||
"token": "abcde",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
"""Test Zeroconf component setup process."""
|
||||
import pytest
|
||||
from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
|
||||
from zeroconf import (
|
||||
BadTypeInNameException,
|
||||
InterfaceChoice,
|
||||
IPVersion,
|
||||
ServiceInfo,
|
||||
ServiceStateChange,
|
||||
)
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
|
||||
@@ -175,6 +181,20 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf):
|
||||
assert mock_zeroconf.called_with()
|
||||
|
||||
|
||||
async def test_service_with_invalid_name(hass, mock_zeroconf, caplog):
|
||||
"""Test we do not crash on service with an invalid name."""
|
||||
with patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
) as mock_service_browser:
|
||||
mock_zeroconf.get_service_info.side_effect = BadTypeInNameException
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_service_browser.mock_calls) == 1
|
||||
assert "Failed to get info for device name" in caplog.text
|
||||
|
||||
|
||||
async def test_homekit_match_partial_space(hass, mock_zeroconf):
|
||||
"""Test configured options for a device are loaded via config entry."""
|
||||
with patch.dict(
|
||||
|
||||
25
tests/fixtures/ozw/light_pure_rgb.json
vendored
Normal file
25
tests/fixtures/ozw/light_pure_rgb.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"topic": "OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/",
|
||||
"payload": {
|
||||
"Label": "Color",
|
||||
"Value": "#ff00000000",
|
||||
"Units": "#RRGGBBWW",
|
||||
"ValueSet": false,
|
||||
"ValuePolled": false,
|
||||
"ChangeVerified": false,
|
||||
"Min": 0,
|
||||
"Max": 0,
|
||||
"Type": "String",
|
||||
"Instance": 1,
|
||||
"CommandClass": "COMMAND_CLASS_COLOR",
|
||||
"Index": 0,
|
||||
"Node": 7,
|
||||
"Genre": "User",
|
||||
"Help": "Color (in RGB format)",
|
||||
"ValueIDKey": 122470423,
|
||||
"ReadOnly": false,
|
||||
"WriteOnly": false,
|
||||
"Event": "valueAdded",
|
||||
"TimeStamp": 1597142799
|
||||
}
|
||||
}
|
||||
131
tests/fixtures/ozw/light_pure_rgb_dimmer_dump.csv
vendored
Normal file
131
tests/fixtures/ozw/light_pure_rgb_dimmer_dump.csv
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user