Merge pull request #54368 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-08-09 20:50:56 -07:00 committed by GitHub
commit a21e3aed77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 337 additions and 169 deletions

View File

@ -15,7 +15,6 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers import entity_platform
from .const import (
@ -166,19 +165,22 @@ class AdvantageAirZone(AdvantageAirClimateEntity):
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
)
async def async_added_to_hass(self):
"""When entity is added to hass."""
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
@callback
def _update_callback(self) -> None:
"""Load data from integration."""
self._attr_current_temperature = self._zone["measuredTemp"]
self._attr_target_temperature = self._zone["setTemp"]
self._attr_hvac_mode = HVAC_MODE_OFF
@property
def hvac_mode(self):
"""Return the current state as HVAC mode."""
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
self._attr_hvac_mode = HVAC_MODE_FAN_ONLY
self.async_write_ha_state()
return HVAC_MODE_FAN_ONLY
return HVAC_MODE_OFF
@property
def current_temperature(self):
"""Return the current temperature."""
return self._zone["measuredTemp"]
@property
def target_temperature(self):
"""Return the target temperature."""
return self._zone["setTemp"]
async def async_set_hvac_mode(self, hvac_mode):
"""Set the HVAC Mode and State."""

View File

@ -67,8 +67,6 @@ async def async_setup_entry(
class AgentCamera(MjpegCamera):
"""Representation of an Agent Device Stream."""
_attr_supported_features = SUPPORT_ON_OFF
def __init__(self, device):
"""Initialize as a subclass of MjpegCamera."""
device_info = {
@ -80,7 +78,6 @@ class AgentCamera(MjpegCamera):
self._removed = False
self._attr_name = f"{device.client.name} {device.name}"
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
self._attr_should_poll = True
super().__init__(device_info)
self._attr_device_info = {
"identifiers": {(AGENT_DOMAIN, self.unique_id)},
@ -102,10 +99,10 @@ class AgentCamera(MjpegCamera):
if self.device.client.is_available and not self._removed:
_LOGGER.error("%s lost", self.name)
self._removed = True
self._attr_available = self.device.client.is_available
self._attr_icon = "mdi:camcorder-off"
if self.is_on:
self._attr_icon = "mdi:camcorder"
self._attr_available = self.device.client.is_available
self._attr_extra_state_attributes = {
ATTR_ATTRIBUTION: ATTRIBUTION,
"editable": False,
@ -117,6 +114,11 @@ class AgentCamera(MjpegCamera):
"alerts_enabled": self.device.alerts_active,
}
@property
def should_poll(self) -> bool:
"""Update the state periodically."""
return True
@property
def is_recording(self) -> bool:
"""Return whether the monitor is recording."""
@ -137,6 +139,11 @@ class AgentCamera(MjpegCamera):
"""Return True if entity is connected."""
return self.device.connected
@property
def supported_features(self) -> int:
"""Return supported features."""
return SUPPORT_ON_OFF
@property
def is_on(self) -> bool:
"""Return true if on."""

View File

@ -111,13 +111,13 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
def _fault_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._attr_state = 1
self._attr_is_on = True
self.schedule_update_ha_state()
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or (int(zone) == self._zone_number and not self._loop):
self._attr_state = 0
self._attr_is_on = False
self.schedule_update_ha_state()
def _rfx_message_callback(self, message):
@ -125,7 +125,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
if self._rfid and message and message.serial_number == self._rfid:
rfstate = message.value
if self._loop:
self._attr_state = 1 if message.loop[self._loop - 1] else 0
self._attr_is_on = bool(message.loop[self._loop - 1])
attr = {CONF_ZONE_NUMBER: self._zone_number}
if self._rfid and rfstate is not None:
attr[ATTR_RF_BIT0] = bool(rfstate & 0x01)
@ -150,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
message.channel,
message.value,
)
self._attr_state = message.value
self._attr_is_on = bool(message.value)
self.schedule_update_ha_state()

View File

@ -93,9 +93,10 @@ class AquaLogicSensor(SensorEntity):
if panel is not None:
if panel.is_metric:
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0]
self._attr_state = getattr(panel, self._type)
self.async_write_ha_state()
else:
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1]
self._attr_state = getattr(panel, self._type)
self.async_write_ha_state()
else:
self._attr_unit_of_measurement = None

View File

@ -14,12 +14,13 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
POWER_WATT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.util import Throttle, dt as dt_util
_LOGGER = logging.getLogger(__name__)
@ -87,12 +88,16 @@ class AtomeData:
self._is_connected = None
self._day_usage = None
self._day_price = None
self._day_last_reset = None
self._week_usage = None
self._week_price = None
self._week_last_reset = None
self._month_usage = None
self._month_price = None
self._month_last_reset = None
self._year_usage = None
self._year_price = None
self._year_last_reset = None
@property
def live_power(self):
@ -137,6 +142,11 @@ class AtomeData:
"""Return latest daily usage value."""
return self._day_price
@property
def day_last_reset(self):
"""Return latest daily last reset."""
return self._day_last_reset
@Throttle(DAILY_SCAN_INTERVAL)
def update_day_usage(self):
"""Return current daily power usage."""
@ -144,6 +154,7 @@ class AtomeData:
values = self.atome_client.get_consumption(DAILY_TYPE)
self._day_usage = values["total"] / 1000
self._day_price = values["price"]
self._day_last_reset = dt_util.parse_datetime(values["startPeriod"])
_LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage)
except KeyError as error:
@ -159,6 +170,11 @@ class AtomeData:
"""Return latest weekly usage value."""
return self._week_price
@property
def week_last_reset(self):
"""Return latest weekly last reset value."""
return self._week_last_reset
@Throttle(WEEKLY_SCAN_INTERVAL)
def update_week_usage(self):
"""Return current weekly power usage."""
@ -166,6 +182,7 @@ class AtomeData:
values = self.atome_client.get_consumption(WEEKLY_TYPE)
self._week_usage = values["total"] / 1000
self._week_price = values["price"]
self._week_last_reset = dt_util.parse_datetime(values["startPeriod"])
_LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage)
except KeyError as error:
@ -181,6 +198,11 @@ class AtomeData:
"""Return latest monthly usage value."""
return self._month_price
@property
def month_last_reset(self):
"""Return latest monthly last reset value."""
return self._month_last_reset
@Throttle(MONTHLY_SCAN_INTERVAL)
def update_month_usage(self):
"""Return current monthly power usage."""
@ -188,6 +210,7 @@ class AtomeData:
values = self.atome_client.get_consumption(MONTHLY_TYPE)
self._month_usage = values["total"] / 1000
self._month_price = values["price"]
self._month_last_reset = dt_util.parse_datetime(values["startPeriod"])
_LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage)
except KeyError as error:
@ -203,6 +226,11 @@ class AtomeData:
"""Return latest yearly usage value."""
return self._year_price
@property
def year_last_reset(self):
"""Return latest yearly last reset value."""
return self._year_last_reset
@Throttle(YEARLY_SCAN_INTERVAL)
def update_year_usage(self):
"""Return current yearly power usage."""
@ -210,6 +238,7 @@ class AtomeData:
values = self.atome_client.get_consumption(YEARLY_TYPE)
self._year_usage = values["total"] / 1000
self._year_price = values["price"]
self._year_last_reset = dt_util.parse_datetime(values["startPeriod"])
_LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage)
except KeyError as error:
@ -219,19 +248,19 @@ class AtomeData:
class AtomeSensor(SensorEntity):
"""Representation of a sensor entity for Atome."""
_attr_device_class = DEVICE_CLASS_POWER
def __init__(self, data, name, sensor_type):
"""Initialize the sensor."""
self._attr_name = name
self._data = data
self._sensor_type = sensor_type
self._attr_state_class = STATE_CLASS_MEASUREMENT
if sensor_type == LIVE_TYPE:
self._attr_device_class = DEVICE_CLASS_POWER
self._attr_unit_of_measurement = POWER_WATT
self._attr_state_class = STATE_CLASS_MEASUREMENT
else:
self._attr_device_class = DEVICE_CLASS_ENERGY
self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
def update(self):
@ -247,6 +276,9 @@ class AtomeSensor(SensorEntity):
}
else:
self._attr_state = getattr(self._data, f"{self._sensor_type}_usage")
self._attr_last_reset = dt_util.as_utc(
getattr(self._data, f"{self._sensor_type}_last_reset")
)
self._attr_extra_state_attributes = {
"price": getattr(self._data, f"{self._sensor_type}_price")
}

View File

@ -203,29 +203,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class BluesoundPlayer(MediaPlayerEntity):
"""Representation of a Bluesound Player."""
_attr_media_content_type = MEDIA_TYPE_MUSIC
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
def __init__(self, hass, host, port=None, name=None, init_callback=None):
"""Initialize the media player."""
self.host = host
self._hass = hass
self.port = port
self._polling_session = async_get_clientsession(hass)
self._polling_task = None # The actual polling task.
self._attr_name = name
self._name = name
self._icon = None
self._capture_items = []
self._services_items = []
self._preset_items = []
self._sync_status = {}
self._status = None
self._is_online = None
self._last_status_update = None
self._is_online = False
self._retry_remove = None
self._muted = False
self._master = None
self._group_name = None
self._bluesound_device_name = None
self._is_master = False
self._group_name = None
self._group_list = []
self._bluesound_device_name = None
self._init_callback = init_callback
if self.port is None:
self.port = DEFAULT_PORT
class _TimeoutException(Exception):
pass
@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
self._sync_status = resp["SyncStatus"].copy()
if not self.name:
self._attr_name = self._sync_status.get("@name", self.host)
if not self._name:
self._name = self._sync_status.get("@name", self.host)
if not self._bluesound_device_name:
self._bluesound_device_name = self._sync_status.get("@name", self.host)
if not self.icon:
self._attr_icon = self._sync_status.get("@icon", self.host)
if not self._icon:
self._icon = self._sync_status.get("@icon", self.host)
master = self._sync_status.get("master")
if master is not None:
@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity):
await self.async_update_status()
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self.name)
_LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
_LOGGER.debug("Stopping the polling of node %s", self.name)
_LOGGER.debug("Stopping the polling of node %s", self._name)
except Exception:
_LOGGER.exception("Unexpected error in %s", self.name)
_LOGGER.exception("Unexpected error in %s", self._name)
raise
def start_polling(self):
@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity):
if response.status == HTTP_OK:
result = await response.text()
self._is_online = True
self._attr_media_position_updated_at = dt_util.utcnow()
self._last_status_update = dt_util.utcnow()
self._status = xmltodict.parse(result)["status"].copy()
group_name = self._status.get("groupName")
@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity):
except (asyncio.TimeoutError, ClientError):
self._is_online = False
self._attr_media_position_updated_at = None
self._last_status_update = None
self._status = None
self.async_write_ha_state()
_LOGGER.info("Client connection error, marking %s as offline", self.name)
_LOGGER.info("Client connection error, marking %s as offline", self._name)
raise
self.update_state_attr()
def update_state_attr(self):
"""Update state attributes."""
if self._status is None:
self._attr_state = STATE_OFF
self._attr_supported_features = 0
elif self.is_grouped and not self.is_master:
self._attr_state = STATE_GROUPED
self._attr_supported_features = (
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
)
else:
status = self._status.get("state")
self._attr_state = STATE_IDLE
if status in ("pause", "stop"):
self._attr_state = STATE_PAUSED
elif status in ("stream", "play"):
self._attr_state = STATE_PLAYING
supported = SUPPORT_CLEAR_PLAYLIST
if self._status.get("indexing", "0") == "0":
supported = (
supported
| SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_PLAY
| SUPPORT_SELECT_SOURCE
| SUPPORT_SHUFFLE_SET
)
if self.volume_level is not None and self.volume_level >= 0:
supported = (
supported
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
)
if self._status.get("canSeek", "") == "1":
supported = supported | SUPPORT_SEEK
self._attr_supported_features = supported
self._attr_extra_state_attributes = {}
if self._group_list:
self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master
self._attr_shuffle = self._status.get("shuffle", "0") == "1"
async def async_trigger_sync_on_all(self):
"""Trigger sync status update on all devices."""
@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity):
return self._services_items
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def state(self):
"""Return the state of the device."""
if self._status is None:
return STATE_OFF
if self.is_grouped and not self.is_master:
return STATE_GROUPED
status = self._status.get("state")
if status in ("pause", "stop"):
return STATE_PAUSED
if status in ("stream", "play"):
return STATE_PLAYING
return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
mediastate = self.state
if self.media_position_updated_at is None or mediastate == STATE_IDLE:
if self._last_status_update is None or mediastate == STATE_IDLE:
return None
position = self._status.get("secs")
@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity):
position = float(position)
if mediastate == STATE_PLAYING:
position += (
dt_util.utcnow() - self.media_position_updated_at
).total_seconds()
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
return position
@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
return float(duration)
@property
def media_position_updated_at(self):
"""Last time status was updated."""
return self._last_status_update
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity):
mute = bool(int(mute))
return mute
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def bluesound_device_name(self):
"""Return the device name as returned by the device."""
return self._bluesound_device_name
@property
def icon(self):
"""Return the icon of the device."""
return self._icon
@property
def source_list(self):
"""List of available input sources."""
@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
@property
def is_master(self) -> bool:
def supported_features(self):
"""Flag of media commands that are supported."""
if self._status is None:
return 0
if self.is_grouped and not self.is_master:
return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
supported = SUPPORT_CLEAR_PLAYLIST
if self._status.get("indexing", "0") == "0":
supported = (
supported
| SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_PLAY
| SUPPORT_SELECT_SOURCE
| SUPPORT_SHUFFLE_SET
)
current_vol = self.volume_level
if current_vol is not None and current_vol >= 0:
supported = (
supported
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
)
if self._status.get("canSeek", "") == "1":
supported = supported | SUPPORT_SEEK
return supported
@property
def is_master(self):
"""Return true if player is a coordinator."""
return self._is_master
@property
def is_grouped(self) -> bool:
def is_grouped(self):
"""Return true if player is a coordinator."""
return self._master is not None or self._is_master
@property
def shuffle(self):
"""Return true if shuffle is active."""
return self._status.get("shuffle", "0") == "1"
async def async_join(self, master):
"""Join the player to a group."""
master_device = [
@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity):
else:
_LOGGER.error("Master not found %s", master_device)
@property
def extra_state_attributes(self):
"""List members in group."""
attributes = {}
if self._group_list:
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
attributes[ATTR_MASTER] = self._is_master
return attributes
def rebuild_bluesound_group(self):
"""Rebuild the list of entities in speaker group."""
if self._group_name is None:

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.7.16"],
"requirements": ["bimmer_connected==0.7.18"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@ -66,7 +66,7 @@ DEFAULT_FORECAST_TYPE = DAILY
DOMAIN = "climacell"
ATTRIBUTION = "Powered by ClimaCell"
MAX_REQUESTS_PER_DAY = 500
MAX_REQUESTS_PER_DAY = 100
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}

View File

@ -2,7 +2,7 @@
"domain": "cloud",
"name": "Home Assistant Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud",
"requirements": ["hass-nabucasa==0.44.0"],
"requirements": ["hass-nabucasa==0.45.1"],
"dependencies": ["http", "webhook"],
"after_dependencies": ["google_assistant", "alexa"],
"codeowners": ["@home-assistant/cloud"],

View File

@ -2,7 +2,9 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20210804.0"],
"requirements": [
"home-assistant-frontend==20210809.0"
],
"dependencies": [
"api",
"auth",
@ -15,6 +17,8 @@
"system_log",
"websocket_api"
],
"codeowners": ["@home-assistant/frontend"],
"codeowners": [
"@home-assistant/frontend"
],
"quality_scale": "internal"
}
}

View File

@ -63,12 +63,19 @@ def async_setup_forwarded(
an HTTP 400 status code is thrown.
"""
try:
from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
except ImportError:
remote = None
@middleware
async def forwarded_middleware(
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
) -> StreamResponse:
"""Process forwarded data by a reverse proxy."""
overrides: dict[str, str] = {}
# Skip requests from Remote UI
if remote is not None and remote.is_cloud_request.get():
return await handler(request)
# Handle X-Forwarded-For
forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, [])
@ -120,6 +127,8 @@ def async_setup_forwarded(
)
raise HTTPBadRequest from err
overrides: dict[str, str] = {}
# Find the last trusted index in the X-Forwarded-For list
forwarded_for_index = 0
for forwarded_ip in forwarded_for:

View File

@ -71,7 +71,7 @@ class ShadeEntity(HDEntity):
"name": self._shade_name,
"suggested_area": self._room_name,
"manufacturer": MANUFACTURER,
"model": self._shade.raw_data[ATTR_TYPE],
"model": str(self._shade.raw_data[ATTR_TYPE]),
"via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]),
}

View File

@ -145,6 +145,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
)
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._unit_of_measurement = state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
)
@callback
def calc_integration(event):
"""Handle the sensor state changes."""

View File

@ -141,9 +141,9 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
self._poolid = self.coordinator.data[poolidx]["id"]
pooldata = self._pooldata()
self._unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}"
self._attr_unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}"
self._device_name = pooldata["name"]
self._name = f"{self._device_name} {description.name}"
self._attr_name = f"{self._device_name} {description.name}"
def _pooldata(self):
"""Get pool data dict."""
@ -168,11 +168,6 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
"""Last value of the sensor."""
return self._devdata()["value"]
@property
def unique_id(self):
"""Return the unique ID of this entity."""
return self._unique_id
@property
def device_info(self):
"""Return the device info for the sensor."""

View File

@ -431,7 +431,7 @@ class SimpliSafeEntity(CoordinatorEntity):
self._attr_device_info = {
"identifiers": {(DOMAIN, system.system_id)},
"manufacturer": "SimpliSafe",
"model": system.version,
"model": str(system.version),
"name": name,
"via_device": (DOMAIN, system.serial),
}

View File

@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.23.2"],
"requirements": ["soco==0.23.3"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "zeroconf"],
"zeroconf": ["_sonos._tcp.local."],

View File

@ -496,9 +496,7 @@ class SonosSpeaker:
self.async_write_entity_states()
async def async_unseen(
self, now: datetime.datetime | None = None, will_reconnect: bool = False
) -> None:
async def async_unseen(self, now: datetime.datetime | None = None) -> None:
"""Make this player unavailable when it was not seen recently."""
if self._seen_timer:
self._seen_timer()
@ -527,9 +525,8 @@ class SonosSpeaker:
await self.async_unsubscribe()
if not will_reconnect:
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
self.async_write_entity_states()
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
self.async_write_entity_states()
async def async_rebooted(self, soco: SoCo) -> None:
"""Handle a detected speaker reboot."""
@ -538,8 +535,24 @@ class SonosSpeaker:
self.zone_name,
soco,
)
await self.async_unseen(will_reconnect=True)
await self.async_seen(soco)
await self.async_unsubscribe()
self.soco = soco
await self.async_subscribe()
if self._seen_timer:
self._seen_timer()
self._seen_timer = self.hass.helpers.event.async_call_later(
SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen
)
if not self._poll_timer:
self._poll_timer = self.hass.helpers.event.async_track_time_interval(
partial(
async_dispatcher_send,
self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
),
SCAN_INTERVAL,
)
self.async_write_entity_states()
#
# Battery management

View File

@ -615,6 +615,10 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
**kwargs,
) -> None:
"""Turn the device on."""
result = await self._try_command(
"Turning the miio device on failed.", self._device.on
)
# Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented
if speed:
await self.async_set_speed(speed)
@ -623,10 +627,6 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
await self.async_set_percentage(percentage)
if preset_mode:
await self.async_set_preset_mode(preset_mode)
else:
result = await self._try_command(
"Turning the miio device on failed.", self._device.on
)
if result:
self._state = True
@ -1138,6 +1138,7 @@ class XiaomiAirFresh(XiaomiGenericDevice):
self._speed_list = OPERATION_MODES_AIRFRESH
self._speed_count = 4
self._preset_modes = PRESET_MODES_AIRFRESH
self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}
)

View File

@ -142,7 +142,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
zc_args: dict = {}
adapters = await network.async_get_adapters(hass)
if _async_use_default_interface(adapters):
ipv6 = True
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only
else:
zc_args["ip_version"] = IPVersion.All
if not ipv6 and _async_use_default_interface(adapters):
zc_args["interfaces"] = InterfaceChoice.Default
else:
interfaces = zc_args["interfaces"] = []
@ -158,13 +166,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
if adapter["ipv6"] and adapter["index"] not in interfaces:
interfaces.append(adapter["index"])
ipv6 = True
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only
else:
zc_args["ip_version"] = IPVersion.All
aio_zc = await _async_get_instance(hass, **zc_args)
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
zeroconf_types, homekit_models = await asyncio.gather(

View File

@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.33.4"],
"requirements": ["zeroconf==0.34.3"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",

View File

@ -301,7 +301,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
# fallback to setting the color(s) one by one if multicolor fails
# not sure this is needed at all, but just in case
for color, value in colors.items():
await self._async_set_color(color, value, zwave_transition)
await self._async_set_color(color, value)
async def _async_set_color(
self,

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "4"
PATCH_VERSION: Final = "5"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -16,8 +16,8 @@ cryptography==3.3.2
defusedxml==0.7.1
distro==1.5.0
emoji==1.2.0
hass-nabucasa==0.44.0
home-assistant-frontend==20210804.0
hass-nabucasa==0.45.1
home-assistant-frontend==20210809.0
httpx==0.18.2
ifaddr==0.1.7
jinja2==3.0.1
@ -33,7 +33,7 @@ sqlalchemy==1.4.17
voluptuous-serialize==2.4.0
voluptuous==0.12.1
yarl==1.6.3
zeroconf==0.33.4
zeroconf==0.34.3
pycryptodome>=3.6.6

View File

@ -365,7 +365,7 @@ beautifulsoup4==4.9.3
bellows==0.26.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.16
bimmer_connected==0.7.18
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@ -750,7 +750,7 @@ habitipy==0.2.0
hangups==0.4.14
# homeassistant.components.cloud
hass-nabucasa==0.44.0
hass-nabucasa==0.45.1
# homeassistant.components.splunk
hass_splunk==0.1.1
@ -783,7 +783,7 @@ hole==0.5.1
holidays==0.11.2
# homeassistant.components.frontend
home-assistant-frontend==20210804.0
home-assistant-frontend==20210809.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -2152,7 +2152,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3
# homeassistant.components.sonos
soco==0.23.2
soco==0.23.3
# homeassistant.components.solaredge_local
solaredge-local==0.2.0
@ -2439,7 +2439,7 @@ zeep[async]==4.0.0
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.33.4
zeroconf==0.34.3
# homeassistant.components.zha
zha-quirks==0.0.59

View File

@ -220,7 +220,7 @@ base36==0.1.1
bellows==0.26.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.16
bimmer_connected==0.7.18
# homeassistant.components.blebox
blebox_uniapi==1.3.3
@ -428,7 +428,7 @@ habitipy==0.2.0
hangups==0.4.14
# homeassistant.components.cloud
hass-nabucasa==0.44.0
hass-nabucasa==0.45.1
# homeassistant.components.tasmota
hatasmota==0.2.20
@ -449,7 +449,7 @@ hole==0.5.1
holidays==0.11.2
# homeassistant.components.frontend
home-assistant-frontend==20210804.0
home-assistant-frontend==20210809.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1177,7 +1177,7 @@ smarthab==0.21
smhi-pkg==1.0.15
# homeassistant.components.sonos
soco==0.23.2
soco==0.23.3
# homeassistant.components.solaredge
solaredge==0.0.2
@ -1341,7 +1341,7 @@ youless-api==0.10
zeep[async]==4.0.0
# homeassistant.components.zeroconf
zeroconf==0.33.4
zeroconf==0.34.3
# homeassistant.components.zha
zha-quirks==0.0.59

View File

@ -1,5 +1,6 @@
"""Test real forwarded middleware."""
from ipaddress import ip_network
from unittest.mock import Mock, patch
from aiohttp import web
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
@ -441,3 +442,22 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog):
assert resp.status == 400
assert "Empty value received in X-Forward-Host header" in caplog.text
async def test_x_forwarded_cloud(aiohttp_client, caplog):
"""Test that cloud requests are not processed."""
app = web.Application()
app.router.add_get("/", mock_handler)
async_setup_forwarded(app, True, [ip_network("127.0.0.1")])
mock_api_client = await aiohttp_client(app)
with patch(
"hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))
):
resp = await mock_api_client.get(
"/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""}
)
# This request would normally fail because it's invalid, now it works.
assert resp.status == 200

View File

@ -73,6 +73,7 @@ async def test_restore_state(hass: HomeAssistant) -> None:
{
"last_reset": "2019-10-06T21:00:00",
"device_class": DEVICE_CLASS_ENERGY,
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
},
),
),

View File

@ -794,11 +794,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero
), patch(
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
), patch(
"socket.if_nametoindex",
side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get(
iface, 0
),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -827,3 +822,45 @@ async def test_get_announced_addresses(hass, mock_async_zeroconf):
first_ip = ip_address("192.168.1.5").packed
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
assert actual[0] == first_ip and set(actual) == expected
_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [
{
"auto": True,
"default": True,
"enabled": True,
"index": 1,
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
"ipv6": [
{
"address": "fe80::dead:beef:dead:beef",
"network_prefix": 64,
"flowinfo": 1,
"scope_id": 3,
}
],
"name": "eth1",
}
]
async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf):
"""Test interfaces are explicitly set when IPv6 is present."""
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
hass.config_entries.flow, "async_init"
), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
), patch(
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert mock_zc.mock_calls[0] == call(
interfaces=["192.168.1.5", 1], ip_version=IPVersion.All
)

View File

@ -1021,8 +1021,7 @@ async def test_multicast_set_value_options(
],
ATTR_COMMAND_CLASS: 51,
ATTR_PROPERTY: "targetColor",
ATTR_PROPERTY_KEY: 2,
ATTR_VALUE: 2,
ATTR_VALUE: '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }',
ATTR_OPTIONS: {"transitionDuration": 1},
},
blocking=True,
@ -1038,9 +1037,11 @@ async def test_multicast_set_value_options(
assert args["valueId"] == {
"commandClass": 51,
"property": "targetColor",
"propertyKey": 2,
}
assert args["value"] == 2
assert (
args["value"]
== '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }'
)
assert args["options"] == {"transitionDuration": 1}
client.async_send_command.reset_mock()

View File

@ -267,8 +267,7 @@
"min": 0,
"max": 255,
"label": "Target value (Warm White)",
"description": "The target value of the Warm White color.",
"valueChangeOptions": ["transitionDuration"]
"description": "The target value of the Warm White color."
}
},
{
@ -286,8 +285,7 @@
"min": 0,
"max": 255,
"label": "Target value (Cold White)",
"description": "The target value of the Cold White color.",
"valueChangeOptions": ["transitionDuration"]
"description": "The target value of the Cold White color."
}
},
{
@ -305,8 +303,7 @@
"min": 0,
"max": 255,
"label": "Target value (Red)",
"description": "The target value of the Red color.",
"valueChangeOptions": ["transitionDuration"]
"description": "The target value of the Red color."
}
},
{
@ -324,8 +321,7 @@
"min": 0,
"max": 255,
"label": "Target value (Green)",
"description": "The target value of the Green color.",
"valueChangeOptions": ["transitionDuration"]
"description": "The target value of the Green color."
}
},
{
@ -343,8 +339,7 @@
"min": 0,
"max": 255,
"label": "Target value (Blue)",
"description": "The target value of the Blue color.",
"valueChangeOptions": ["transitionDuration"]
"description": "The target value of the Blue color."
}
},
{