Merge pull request #75147 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-07-13 15:25:35 -07:00 committed by GitHub
commit 60e170c863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 257 additions and 125 deletions

View File

@ -2,7 +2,7 @@
"domain": "aladdin_connect", "domain": "aladdin_connect",
"name": "Aladdin Connect", "name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["AIOAladdinConnect==0.1.21"], "requirements": ["AIOAladdinConnect==0.1.23"],
"codeowners": ["@mkmer"], "codeowners": ["@mkmer"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aladdin_connect"], "loggers": ["aladdin_connect"],

View File

@ -3,7 +3,6 @@ from http import HTTPStatus
import json import json
import logging import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests import requests
import voluptuous as vol import voluptuous as vol
@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
BASE_API_URL = "https://rest.clicksend.com/v3" BASE_API_URL = "https://rest.clicksend.com/v3"
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
CONF_LANGUAGE = "language" CONF_LANGUAGE = "language"
CONF_VOICE = "voice" CONF_VOICE = "voice"

View File

@ -7,20 +7,24 @@ from pyecobee.const import ECOBEE_STATE_UNKNOWN
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TEMP, ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT from homeassistant.const import (
LENGTH_METERS,
PRESSURE_HPA,
SPEED_METERS_PER_SECOND,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.pressure import convert as pressure_convert
from .const import ( from .const import (
DOMAIN, DOMAIN,
@ -49,6 +53,11 @@ async def async_setup_entry(
class EcobeeWeather(WeatherEntity): class EcobeeWeather(WeatherEntity):
"""Representation of Ecobee weather data.""" """Representation of Ecobee weather data."""
_attr_native_pressure_unit = PRESSURE_HPA
_attr_native_temperature_unit = TEMP_FAHRENHEIT
_attr_native_visibility_unit = LENGTH_METERS
_attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND
def __init__(self, data, name, index): def __init__(self, data, name, index):
"""Initialize the Ecobee weather platform.""" """Initialize the Ecobee weather platform."""
self.data = data self.data = data
@ -101,7 +110,7 @@ class EcobeeWeather(WeatherEntity):
return None return None
@property @property
def temperature(self): def native_temperature(self):
"""Return the temperature.""" """Return the temperature."""
try: try:
return float(self.get_forecast(0, "temperature")) / 10 return float(self.get_forecast(0, "temperature")) / 10
@ -109,18 +118,10 @@ class EcobeeWeather(WeatherEntity):
return None return None
@property @property
def temperature_unit(self): def native_pressure(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def pressure(self):
"""Return the pressure.""" """Return the pressure."""
try: try:
pressure = self.get_forecast(0, "pressure") pressure = self.get_forecast(0, "pressure")
if not self.hass.config.units.is_metric:
pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG)
return round(pressure, 2)
return round(pressure) return round(pressure)
except ValueError: except ValueError:
return None return None
@ -134,15 +135,15 @@ class EcobeeWeather(WeatherEntity):
return None return None
@property @property
def visibility(self): def native_visibility(self):
"""Return the visibility.""" """Return the visibility."""
try: try:
return int(self.get_forecast(0, "visibility")) / 1000 return int(self.get_forecast(0, "visibility"))
except ValueError: except ValueError:
return None return None
@property @property
def wind_speed(self): def native_wind_speed(self):
"""Return the wind speed.""" """Return the wind speed."""
try: try:
return int(self.get_forecast(0, "windSpeed")) return int(self.get_forecast(0, "windSpeed"))
@ -202,13 +203,13 @@ def _process_forecast(json):
json["weatherSymbol"] json["weatherSymbol"]
] ]
if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: if json["tempHigh"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 forecast[ATTR_FORECAST_NATIVE_TEMP] = float(json["tempHigh"]) / 10
if json["tempLow"] != ECOBEE_STATE_UNKNOWN: if json["tempLow"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = float(json["tempLow"]) / 10
if json["windBearing"] != ECOBEE_STATE_UNKNOWN: if json["windBearing"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"])
if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: if json["windSpeed"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = int(json["windSpeed"])
except (ValueError, IndexError, KeyError): except (ValueError, IndexError, KeyError):
return None return None

View File

@ -2,7 +2,7 @@
"domain": "frontier_silicon", "domain": "frontier_silicon",
"name": "Frontier Silicon", "name": "Frontier Silicon",
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
"requirements": ["afsapi==0.2.5"], "requirements": ["afsapi==0.2.6"],
"codeowners": ["@wlcrs"], "codeowners": ["@wlcrs"],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -179,11 +179,14 @@ class AFSAPIDevice(MediaPlayerEntity):
self._attr_media_artist = await afsapi.get_play_artist() self._attr_media_artist = await afsapi.get_play_artist()
self._attr_media_album_name = await afsapi.get_play_album() self._attr_media_album_name = await afsapi.get_play_album()
self._attr_source = (await afsapi.get_mode()).label radio_mode = await afsapi.get_mode()
self._attr_source = radio_mode.label if radio_mode is not None else None
self._attr_is_volume_muted = await afsapi.get_mute() self._attr_is_volume_muted = await afsapi.get_mute()
self._attr_media_image_url = await afsapi.get_play_graphic() self._attr_media_image_url = await afsapi.get_play_graphic()
self._attr_sound_mode = (await afsapi.get_eq_preset()).label
eq_preset = await afsapi.get_eq_preset()
self._attr_sound_mode = eq_preset.label if eq_preset is not None else None
volume = await self.fs_device.get_volume() volume = await self.fs_device.get_volume()

View File

@ -3,7 +3,7 @@
"name": "HomematicIP Cloud", "name": "HomematicIP Cloud",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"requirements": ["homematicip==1.0.3"], "requirements": ["homematicip==1.0.4"],
"codeowners": [], "codeowners": [],
"quality_scale": "platinum", "quality_scale": "platinum",
"iot_class": "cloud_push", "iot_class": "cloud_push",

View File

@ -22,7 +22,7 @@ from homeassistant.components.weather import (
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -71,6 +71,9 @@ async def async_setup_entry(
class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
"""Representation of the HomematicIP weather sensor plus & basic.""" """Representation of the HomematicIP weather sensor plus & basic."""
_attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__(self, hap: HomematicipHAP, device) -> None: def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the weather sensor.""" """Initialize the weather sensor."""
super().__init__(hap, device) super().__init__(hap, device)
@ -81,22 +84,17 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
return self._device.label return self._device.label
@property @property
def temperature(self) -> float: def native_temperature(self) -> float:
"""Return the platform temperature.""" """Return the platform temperature."""
return self._device.actualTemperature return self._device.actualTemperature
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property @property
def humidity(self) -> int: def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return self._device.humidity return self._device.humidity
@property @property
def wind_speed(self) -> float: def native_wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return self._device.windSpeed return self._device.windSpeed
@ -129,6 +127,9 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor):
class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
"""Representation of the HomematicIP home weather.""" """Representation of the HomematicIP home weather."""
_attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__(self, hap: HomematicipHAP) -> None: def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize the home weather.""" """Initialize the home weather."""
hap.home.modelType = "HmIP-Home-Weather" hap.home.modelType = "HmIP-Home-Weather"
@ -145,22 +146,17 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
return f"Weather {self._home.location.city}" return f"Weather {self._home.location.city}"
@property @property
def temperature(self) -> float: def native_temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return self._device.weather.temperature return self._device.weather.temperature
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property @property
def humidity(self) -> int: def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return self._device.weather.humidity return self._device.weather.humidity
@property @property
def wind_speed(self) -> float: def native_wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return round(self._device.weather.windSpeed, 1) return round(self._device.weather.windSpeed, 1)

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte", "documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [ "requirements": [
"huawei-lte-api==1.6.0", "huawei-lte-api==1.6.1",
"stringcase==1.2.0", "stringcase==1.2.0",
"url-normalize==1.4.1" "url-normalize==1.4.1"
], ],

View File

@ -455,14 +455,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU):
super().__init__(coordinator, device_info, room_name, shade, name) super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_unique_id = f"{self._shade.id}_top" self._attr_unique_id = f"{self._shade.id}_top"
self._attr_name = f"{self._shade_name} Top" self._attr_name = f"{self._shade_name} Top"
# these shades share a class in parent API
# override open position for top shade
self._shade.open_position = {
ATTR_POSITION1: MIN_POSITION,
ATTR_POSITION2: MAX_POSITION,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
}
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
@ -485,6 +477,21 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU):
# these need to be inverted to report state correctly in HA # these need to be inverted to report state correctly in HA
return hd_position_to_hass(self.positions.secondary, MAX_POSITION) return hd_position_to_hass(self.positions.secondary, MAX_POSITION)
@property
def open_position(self) -> PowerviewShadeMove:
"""Return the open position and required additional positions."""
# these shades share a class in parent API
# override open position for top shade
return PowerviewShadeMove(
{
ATTR_POSITION1: MIN_POSITION,
ATTR_POSITION2: MAX_POSITION,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
},
{},
)
@callback @callback
def _clamp_cover_limit(self, target_hass_position: int) -> int: def _clamp_cover_limit(self, target_hass_position: int) -> int:
"""Dont allow a cover to go into an impossbile position.""" """Dont allow a cover to go into an impossbile position."""

View File

@ -2,7 +2,7 @@
"domain": "ialarm", "domain": "ialarm",
"name": "Antifurto365 iAlarm", "name": "Antifurto365 iAlarm",
"documentation": "https://www.home-assistant.io/integrations/ialarm", "documentation": "https://www.home-assistant.io/integrations/ialarm",
"requirements": ["pyialarm==1.9.0"], "requirements": ["pyialarm==2.2.0"],
"codeowners": ["@RyuzakiKK"], "codeowners": ["@RyuzakiKK"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -86,7 +86,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity):
@property @property
def temperature_unit(self) -> str: def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
if self._insteon_device.properties[CELSIUS].value: if self._insteon_device.configuration[CELSIUS].value:
return TEMP_CELSIUS return TEMP_CELSIUS
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT

View File

@ -4,8 +4,8 @@
"documentation": "https://www.home-assistant.io/integrations/insteon", "documentation": "https://www.home-assistant.io/integrations/insteon",
"dependencies": ["http", "websocket_api"], "dependencies": ["http", "websocket_api"],
"requirements": [ "requirements": [
"pyinsteon==1.1.1", "pyinsteon==1.1.3",
"insteon-frontend-home-assistant==0.1.1" "insteon-frontend-home-assistant==0.2.0"
], ],
"codeowners": ["@teharris1"], "codeowners": ["@teharris1"],
"dhcp": [ "dhcp": [

View File

@ -37,7 +37,7 @@ from .const import (
SHOW_DRIVING, SHOW_DRIVING,
SHOW_MOVING, SHOW_MOVING,
) )
from .coordinator import Life360DataUpdateCoordinator from .coordinator import Life360DataUpdateCoordinator, MissingLocReason
PLATFORMS = [Platform.DEVICE_TRACKER] PLATFORMS = [Platform.DEVICE_TRACKER]
@ -128,6 +128,10 @@ class IntegData:
coordinators: dict[str, Life360DataUpdateCoordinator] = field( coordinators: dict[str, Life360DataUpdateCoordinator] = field(
init=False, default_factory=dict init=False, default_factory=dict
) )
# member_id: missing location reason
missing_loc_reason: dict[str, MissingLocReason] = field(
init=False, default_factory=dict
)
# member_id: ConfigEntry.entry_id # member_id: ConfigEntry.entry_id
tracked_members: dict[str, str] = field(init=False, default_factory=dict) tracked_members: dict[str, str] = field(init=False, default_factory=dict)
logged_circles: list[str] = field(init=False, default_factory=list) logged_circles: list[str] = field(init=False, default_factory=list)

View File

@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
from contextlib import suppress
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Any from typing import Any
from life360 import Life360, Life360Error, LoginError from life360 import Life360, Life360Error, LoginError
@ -33,6 +35,13 @@ from .const import (
) )
class MissingLocReason(Enum):
"""Reason member location information is missing."""
VAGUE_ERROR_REASON = "vague error reason"
EXPLICIT_ERROR_REASON = "explicit error reason"
@dataclass @dataclass
class Life360Place: class Life360Place:
"""Life360 Place data.""" """Life360 Place data."""
@ -99,6 +108,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
max_retries=COMM_MAX_RETRIES, max_retries=COMM_MAX_RETRIES,
authorization=entry.data[CONF_AUTHORIZATION], authorization=entry.data[CONF_AUTHORIZATION],
) )
self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason
async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]:
"""Get data from Life360.""" """Get data from Life360."""
@ -141,10 +151,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
if not int(member["features"]["shareLocation"]): if not int(member["features"]["shareLocation"]):
continue continue
# Note that member may be in more than one circle. If that's the case just member_id = member["id"]
# go ahead and process the newly retrieved data (overwriting the older
# data), since it might be slightly newer than what was retrieved while
# processing another circle.
first = member["firstName"] first = member["firstName"]
last = member["lastName"] last = member["lastName"]
@ -153,16 +160,45 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
else: else:
name = first or last name = first or last
loc = member["location"] cur_missing_reason = self._missing_loc_reason.get(member_id)
if not loc:
if err_msg := member["issues"]["title"]: # Check if location information is missing. This can happen if server
if member["issues"]["dialog"]: # has not heard from member's device in a long time (e.g., has been off
err_msg += f": {member['issues']['dialog']}" # for a long time, or has lost service, etc.)
else: if loc := member["location"]:
err_msg = "Location information missing" with suppress(KeyError):
LOGGER.error("%s: %s", name, err_msg) del self._missing_loc_reason[member_id]
else:
if explicit_reason := member["issues"]["title"]:
if extended_reason := member["issues"]["dialog"]:
explicit_reason += f": {extended_reason}"
# Note that different Circles can report missing location in
# different ways. E.g., one might report an explicit reason and
# another does not. If a vague reason has already been logged but a
# more explicit reason is now available, log that, too.
if (
cur_missing_reason is None
or cur_missing_reason == MissingLocReason.VAGUE_ERROR_REASON
and explicit_reason
):
if explicit_reason:
self._missing_loc_reason[
member_id
] = MissingLocReason.EXPLICIT_ERROR_REASON
err_msg = explicit_reason
else:
self._missing_loc_reason[
member_id
] = MissingLocReason.VAGUE_ERROR_REASON
err_msg = "Location information missing"
LOGGER.error("%s: %s", name, err_msg)
continue continue
# Note that member may be in more than one circle. If that's the case
# just go ahead and process the newly retrieved data (overwriting the
# older data), since it might be slightly newer than what was retrieved
# while processing another circle.
place = loc["name"] or None place = loc["name"] or None
if place: if place:
@ -179,7 +215,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
if self._hass.config.units.is_metric: if self._hass.config.units.is_metric:
speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS)
data.members[member["id"]] = Life360Member( data.members[member_id] = Life360Member(
address, address,
dt_util.utc_from_timestamp(int(loc["since"])), dt_util.utc_from_timestamp(int(loc["since"])),
bool(int(loc["charge"])), bool(int(loc["charge"])),

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests import requests
import voluptuous as vol import voluptuous as vol
@ -144,7 +143,7 @@ class PyLoadAPI:
"""Initialize pyLoad API and set headers needed later.""" """Initialize pyLoad API and set headers needed later."""
self.api_url = api_url self.api_url = api_url
self.status = None self.status = None
self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} self.headers = {"Content-Type": CONTENT_TYPE_JSON}
if username is not None and password is not None: if username is not None and password is not None:
self.payload = {"username": username, "password": password} self.payload = {"username": username, "password": password}

View File

@ -199,7 +199,7 @@ class OptionsFlow(config_entries.OptionsFlow):
if not errors: if not errors:
devices = {} devices = {}
device = { device = {
CONF_DEVICE_ID: device_id, CONF_DEVICE_ID: list(device_id),
} }
devices[self._selected_device_event_code] = device devices[self._selected_device_event_code] = device

View File

@ -29,8 +29,7 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ruckus Unleashed from a config entry.""" """Set up Ruckus Unleashed from a config entry."""
try: try:
ruckus = await hass.async_add_executor_job( ruckus = await Ruckus.create(
Ruckus,
entry.data[CONF_HOST], entry.data[CONF_HOST],
entry.data[CONF_USERNAME], entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
@ -42,10 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
system_info = await hass.async_add_executor_job(ruckus.system_info) system_info = await ruckus.system_info()
registry = device_registry.async_get(hass) registry = device_registry.async_get(hass)
ap_info = await hass.async_add_executor_job(ruckus.ap_info) ap_info = await ruckus.ap_info()
for device in ap_info[API_AP][API_ID].values(): for device in ap_info[API_AP][API_ID].values():
registry.async_get_or_create( registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,

View File

@ -21,22 +21,24 @@ DATA_SCHEMA = vol.Schema(
) )
def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
try: try:
ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) ruckus = await Ruckus.create(
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]
)
except AuthenticationError as error: except AuthenticationError as error:
raise InvalidAuth from error raise InvalidAuth from error
except ConnectionError as error: except ConnectionError as error:
raise CannotConnect from error raise CannotConnect from error
mesh_name = ruckus.mesh_name() mesh_name = await ruckus.mesh_name()
system_info = ruckus.system_info() system_info = await ruckus.system_info()
try: try:
host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL] host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL]
except KeyError as error: except KeyError as error:
@ -58,9 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
info = await self.hass.async_add_executor_job( info = await validate_input(self.hass, user_input)
validate_input, self.hass, user_input
)
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:

View File

@ -37,9 +37,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
async def _fetch_clients(self) -> dict: async def _fetch_clients(self) -> dict:
"""Fetch clients from the API and format them.""" """Fetch clients from the API and format them."""
clients = await self.hass.async_add_executor_job( clients = await self.ruckus.current_active_clients()
self.ruckus.current_active_clients
)
return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]} return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]}
async def _async_update_data(self) -> dict: async def _async_update_data(self) -> dict:

View File

@ -3,7 +3,7 @@
"name": "Ruckus Unleashed", "name": "Ruckus Unleashed",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
"requirements": ["pyruckus==0.12"], "requirements": ["pyruckus==0.16"],
"codeowners": ["@gabe565"], "codeowners": ["@gabe565"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pexpect", "pyruckus"] "loggers": ["pexpect", "pyruckus"]

View File

@ -5,7 +5,7 @@ from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_LOCATION from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import SimpliSafe from . import SimpliSafe
@ -23,6 +23,7 @@ CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = { TO_REDACT = {
CONF_ADDRESS, CONF_ADDRESS,
CONF_CODE,
CONF_CREDIT_CARD, CONF_CREDIT_CARD,
CONF_EXPIRES, CONF_EXPIRES,
CONF_LOCATION, CONF_LOCATION,

View File

@ -69,11 +69,13 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP, SERVICE_VOLUME_UP,
STATE_BUFFERING,
STATE_IDLE, STATE_IDLE,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, STATE_PLAYING,
STATE_STANDBY,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
@ -101,8 +103,10 @@ STATES_ORDER = [
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_OFF, STATE_OFF,
STATE_IDLE, STATE_IDLE,
STATE_STANDBY,
STATE_ON, STATE_ON,
STATE_PAUSED, STATE_PAUSED,
STATE_BUFFERING,
STATE_PLAYING, STATE_PLAYING,
] ]
ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string)

View File

@ -170,7 +170,7 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice:
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
registry_device = device_registry.async_get(device_id) registry_device = device_registry.async_get(device_id)
if not registry_device: if not registry_device:
raise ValueError(f"Device id `{device_id}` not found in registry.") raise KeyError(f"Device id `{device_id}` not found in registry.")
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee_address = list(list(registry_device.identifiers)[0])[1] ieee_address = list(list(registry_device.identifiers)[0])[1]
ieee = zigpy.types.EUI64.convert(ieee_address) ieee = zigpy.types.EUI64.convert(ieee_address)

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows==0.31.0", "bellows==0.31.1",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.77", "zha-quirks==0.0.77",
@ -12,7 +12,7 @@
"zigpy==0.47.2", "zigpy==0.47.2",
"zigpy-xbee==0.15.0", "zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.0", "zigpy-zigate==0.9.0",
"zigpy-znp==0.8.0" "zigpy-znp==0.8.1"
], ],
"usb": [ "usb": [
{ {

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 7 MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -14,7 +14,6 @@ from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT
from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout
import async_timeout import async_timeout
import orjson
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__
@ -23,6 +22,7 @@ from homeassistant.loader import bind_hass
from homeassistant.util import ssl as ssl_util from homeassistant.util import ssl as ssl_util
from .frame import warn_use from .frame import warn_use
from .json import json_dumps
DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR = "aiohttp_connector"
DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify"
@ -98,7 +98,7 @@ def _async_create_clientsession(
"""Create a new ClientSession with kwargs, i.e. for cookies.""" """Create a new ClientSession with kwargs, i.e. for cookies."""
clientsession = aiohttp.ClientSession( clientsession = aiohttp.ClientSession(
connector=_async_get_connector(hass, verify_ssl), connector=_async_get_connector(hass, verify_ssl),
json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), json_serialize=json_dumps,
**kwargs, **kwargs,
) )
# Prevent packages accidentally overriding our default headers # Prevent packages accidentally overriding our default headers

View File

@ -114,3 +114,7 @@ backoff<2.0
# Breaking change in version # Breaking change in version
# https://github.com/samuelcolvin/pydantic/issues/4092 # https://github.com/samuelcolvin/pydantic/issues/4092
pydantic!=1.9.1 pydantic!=1.9.1
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
pubnub!=6.4.0

View File

@ -89,9 +89,6 @@ def install_package(
# This only works if not running in venv # This only works if not running in venv
args += ["--user"] args += ["--user"]
env["PYTHONUSERBASE"] = os.path.abspath(target) env["PYTHONUSERBASE"] = os.path.abspath(target)
# Workaround for incompatible prefix setting
# See http://stackoverflow.com/a/4495175
args += ["--prefix="]
_LOGGER.debug("Running pip command: args=%s", args) _LOGGER.debug("Running pip command: args=%s", args)
with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process:
_, stderr = process.communicate() _, stderr = process.communicate()

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2022.7.3" version = "2022.7.4"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -5,7 +5,7 @@
AEMET-OpenData==0.2.1 AEMET-OpenData==0.2.1
# homeassistant.components.aladdin_connect # homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.21 AIOAladdinConnect==0.1.23
# homeassistant.components.adax # homeassistant.components.adax
Adax-local==0.1.4 Adax-local==0.1.4
@ -89,7 +89,7 @@ adguardhome==0.5.1
advantage_air==0.3.1 advantage_air==0.3.1
# homeassistant.components.frontier_silicon # homeassistant.components.frontier_silicon
afsapi==0.2.5 afsapi==0.2.6
# homeassistant.components.agent_dvr # homeassistant.components.agent_dvr
agent-py==0.0.23 agent-py==0.0.23
@ -393,7 +393,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10 # beewi_smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.31.0 bellows==0.31.1
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.9.6 bimmer_connected==0.9.6
@ -834,7 +834,7 @@ home-assistant-frontend==20220707.0
homeconnect==0.7.1 homeconnect==0.7.1
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==1.0.3 homematicip==1.0.4
# homeassistant.components.home_plus_control # homeassistant.components.home_plus_control
homepluscontrol==0.0.5 homepluscontrol==0.0.5
@ -846,7 +846,7 @@ horimote==0.4.1
httplib2==0.20.4 httplib2==0.20.4
# homeassistant.components.huawei_lte # homeassistant.components.huawei_lte
huawei-lte-api==1.6.0 huawei-lte-api==1.6.1
# homeassistant.components.huisbaasje # homeassistant.components.huisbaasje
huisbaasje-client==0.1.0 huisbaasje-client==0.1.0
@ -891,7 +891,7 @@ influxdb-client==1.24.0
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.insteon # homeassistant.components.insteon
insteon-frontend-home-assistant==0.1.1 insteon-frontend-home-assistant==0.2.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==2.0.1 intellifire4py==2.0.1
@ -1553,13 +1553,13 @@ pyhomematic==0.1.77
pyhomeworks==0.0.6 pyhomeworks==0.0.6
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==1.9.0 pyialarm==2.2.0
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==1.0.0 pyicloud==1.0.0
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.1.1 pyinsteon==1.1.3
# homeassistant.components.intesishome # homeassistant.components.intesishome
pyintesishome==1.8.0 pyintesishome==1.8.0
@ -1780,7 +1780,7 @@ pyrisco==0.3.1
pyrituals==0.0.6 pyrituals==0.0.6
# homeassistant.components.ruckus_unleashed # homeassistant.components.ruckus_unleashed
pyruckus==0.12 pyruckus==0.16
# homeassistant.components.sabnzbd # homeassistant.components.sabnzbd
pysabnzbd==1.1.1 pysabnzbd==1.1.1
@ -2516,7 +2516,7 @@ zigpy-xbee==0.15.0
zigpy-zigate==0.9.0 zigpy-zigate==0.9.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.0 zigpy-znp==0.8.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.47.2 zigpy==0.47.2

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.2.1 AEMET-OpenData==0.2.1
# homeassistant.components.aladdin_connect # homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.21 AIOAladdinConnect==0.1.23
# homeassistant.components.adax # homeassistant.components.adax
Adax-local==0.1.4 Adax-local==0.1.4
@ -308,7 +308,7 @@ base36==0.1.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.31.0 bellows==0.31.1
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.9.6 bimmer_connected==0.9.6
@ -601,7 +601,7 @@ home-assistant-frontend==20220707.0
homeconnect==0.7.1 homeconnect==0.7.1
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==1.0.3 homematicip==1.0.4
# homeassistant.components.home_plus_control # homeassistant.components.home_plus_control
homepluscontrol==0.0.5 homepluscontrol==0.0.5
@ -610,7 +610,7 @@ homepluscontrol==0.0.5
httplib2==0.20.4 httplib2==0.20.4
# homeassistant.components.huawei_lte # homeassistant.components.huawei_lte
huawei-lte-api==1.6.0 huawei-lte-api==1.6.1
# homeassistant.components.huisbaasje # homeassistant.components.huisbaasje
huisbaasje-client==0.1.0 huisbaasje-client==0.1.0
@ -634,7 +634,7 @@ influxdb-client==1.24.0
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.insteon # homeassistant.components.insteon
insteon-frontend-home-assistant==0.1.1 insteon-frontend-home-assistant==0.2.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==2.0.1 intellifire4py==2.0.1
@ -1047,13 +1047,13 @@ pyhiveapi==0.5.13
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==1.9.0 pyialarm==2.2.0
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==1.0.0 pyicloud==1.0.0
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.1.1 pyinsteon==1.1.3
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==2.0.5 pyipma==2.0.5
@ -1211,7 +1211,7 @@ pyrisco==0.3.1
pyrituals==0.0.6 pyrituals==0.0.6
# homeassistant.components.ruckus_unleashed # homeassistant.components.ruckus_unleashed
pyruckus==0.12 pyruckus==0.16
# homeassistant.components.sabnzbd # homeassistant.components.sabnzbd
pysabnzbd==1.1.1 pysabnzbd==1.1.1
@ -1677,7 +1677,7 @@ zigpy-xbee==0.15.0
zigpy-zigate==0.9.0 zigpy-zigate==0.9.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.0 zigpy-znp==0.8.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.47.2 zigpy==0.47.2

View File

@ -132,6 +132,10 @@ backoff<2.0
# Breaking change in version # Breaking change in version
# https://github.com/samuelcolvin/pydantic/issues/4092 # https://github.com/samuelcolvin/pydantic/issues/4092
pydantic!=1.9.1 pydantic!=1.9.1
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
pubnub!=6.4.0
""" """
IGNORE_PRE_COMMIT_HOOK_ID = ( IGNORE_PRE_COMMIT_HOOK_ID = (

View File

@ -867,6 +867,9 @@ async def test_options_configure_rfy_cover_device(hass):
entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"]
== "EU" == "EU"
) )
assert isinstance(
entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list
)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
@ -904,6 +907,9 @@ async def test_options_configure_rfy_cover_device(hass):
entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"]
== "EU" == "EU"
) )
assert isinstance(
entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list
)
def test_get_serial_by_id_no_dir(): def test_get_serial_by_id_no_dir():

View File

@ -31,7 +31,7 @@ async def test_setup_entry_login_error(hass):
"""Test entry setup failed due to login error.""" """Test entry setup failed due to login error."""
entry = mock_config_entry() entry = mock_config_entry()
with patch( with patch(
"homeassistant.components.ruckus_unleashed.Ruckus", "homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=AuthenticationError, side_effect=AuthenticationError,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -45,7 +45,7 @@ async def test_setup_entry_connection_error(hass):
"""Test entry setup failed due to connection error.""" """Test entry setup failed due to connection error."""
entry = mock_config_entry() entry = mock_config_entry()
with patch( with patch(
"homeassistant.components.ruckus_unleashed.Ruckus", "homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=ConnectionError, side_effect=ConnectionError,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)

View File

@ -43,7 +43,9 @@ def api_fixture(api_auth_state, data_subscription, system_v3, websocket):
@pytest.fixture(name="config_entry") @pytest.fixture(name="config_entry")
def config_entry_fixture(hass, config, unique_id): def config_entry_fixture(hass, config, unique_id):
"""Define a config entry.""" """Define a config entry."""
entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) entry = MockConfigEntry(
domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"}
)
entry.add_to_hass(hass) entry.add_to_hass(hass)
return entry return entry

View File

@ -8,7 +8,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
"entry": { "entry": {
"options": {}, "options": {
"code": REDACTED,
},
}, },
"subscription_data": { "subscription_data": {
"system_123": { "system_123": {

View File

@ -370,3 +370,41 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert "Invalid config for [automation]" in caplog.text assert "Invalid config for [automation]" in caplog.text
async def test_exception_no_device(hass, mock_devices, calls, caplog):
"""Test for exception on event triggers firing."""
zigpy_device, zha_device = mock_devices
zigpy_device.device_automation_triggers = {
(SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE},
(DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE},
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE},
(LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD},
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD},
}
await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"device_id": "no_such_device_id",
"domain": "zha",
"platform": "device",
"type": "junk",
"subtype": "junk",
},
"action": {
"service": "test.automation",
"data": {"message": "service called"},
},
}
]
},
)
await hass.async_block_till_done()
assert "Invalid config for [automation]" in caplog.text

View File

@ -19,6 +19,7 @@ from homeassistant.const import (
) )
from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE
import homeassistant.helpers.aiohttp_client as client import homeassistant.helpers.aiohttp_client as client
from homeassistant.util.color import RGBColor
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -215,6 +216,16 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie
assert resp.status == 502 assert resp.status == 502
async def test_sending_named_tuple(hass, aioclient_mock):
"""Test sending a named tuple in json."""
resp = aioclient_mock.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)})
session = client.async_create_clientsession(hass)
resp = await session.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)})
assert resp.status == 200
await resp.json() == {"rgb": RGBColor(4, 3, 2)}
aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2)
async def test_client_session_immutable_headers(hass): async def test_client_session_immutable_headers(hass):
"""Test we can't mutate headers.""" """Test we can't mutate headers."""
session = client.async_get_clientsession(hass) session = client.async_get_clientsession(hass)

View File

@ -2,6 +2,7 @@
import datetime import datetime
import json import json
import time import time
from typing import NamedTuple
import pytest import pytest
@ -13,6 +14,7 @@ from homeassistant.helpers.json import (
json_dumps_sorted, json_dumps_sorted,
) )
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.color import RGBColor
@pytest.mark.parametrize("encoder", (JSONEncoder, ExtendedJSONEncoder)) @pytest.mark.parametrize("encoder", (JSONEncoder, ExtendedJSONEncoder))
@ -96,3 +98,23 @@ def test_json_dumps_tuple_subclass():
tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0))
assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]"
def test_json_dumps_named_tuple_subclass():
"""Test the json dumps a tuple subclass."""
class NamedTupleSubclass(NamedTuple):
"""A NamedTuple subclass."""
name: str
nts = NamedTupleSubclass("a")
assert json_dumps(nts) == '["a"]'
def test_json_dumps_rgb_color_subclass():
"""Test the json dumps of RGBColor."""
rgb = RGBColor(4, 2, 1)
assert json_dumps(rgb) == "[4,2,1]"

View File

@ -111,7 +111,7 @@ class AiohttpClientMocker:
def create_session(self, loop): def create_session(self, loop):
"""Create a ClientSession that is bound to this mocker.""" """Create a ClientSession that is bound to this mocker."""
session = ClientSession(loop=loop) session = ClientSession(loop=loop, json_serialize=json_dumps)
# Setting directly on `session` will raise deprecation warning # Setting directly on `session` will raise deprecation warning
object.__setattr__(session, "_request", self.match_request) object.__setattr__(session, "_request", self.match_request)
return session return session

View File

@ -137,7 +137,6 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv):
"--quiet", "--quiet",
TEST_NEW_REQ, TEST_NEW_REQ,
"--user", "--user",
"--prefix=",
] ]
assert package.install_package(TEST_NEW_REQ, False, target=target) assert package.install_package(TEST_NEW_REQ, False, target=target)
@ -156,7 +155,7 @@ def test_install_target_venv(mock_sys, mock_popen, mock_env_copy, mock_venv):
def test_install_error(caplog, mock_sys, mock_popen, mock_venv): def test_install_error(caplog, mock_sys, mock_popen, mock_venv):
"""Test an install with a target.""" """Test an install that errors out."""
caplog.set_level(logging.WARNING) caplog.set_level(logging.WARNING)
mock_popen.return_value.returncode = 1 mock_popen.return_value.returncode = 1
assert not package.install_package(TEST_NEW_REQ) assert not package.install_package(TEST_NEW_REQ)