mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge pull request #52936 from home-assistant/rc
This commit is contained in:
commit
f4359f98b3
@ -687,6 +687,7 @@ omit =
|
||||
homeassistant/components/netgear_lte/*
|
||||
homeassistant/components/netio/switch.py
|
||||
homeassistant/components/neurio_energy/sensor.py
|
||||
homeassistant/components/nexia/climate.py
|
||||
homeassistant/components/nextcloud/*
|
||||
homeassistant/components/nfandroidtv/notify.py
|
||||
homeassistant/components/niko_home_control/light.py
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Arcam FMJ Receivers",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||
"requirements": ["arcam-fmj==0.5.3"],
|
||||
"requirements": ["arcam-fmj==0.7.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
@ -214,7 +214,6 @@ CC_SENSOR_TYPES = [
|
||||
ATTR_FIELD: CC_ATTR_CLOUD_COVER,
|
||||
ATTR_NAME: "Cloud Cover",
|
||||
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
|
||||
ATTR_SCALE: 1 / 100,
|
||||
},
|
||||
{
|
||||
ATTR_FIELD: CC_ATTR_WIND_GUST,
|
||||
|
@ -207,8 +207,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
||||
distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||
)
|
||||
cloud_cover = self.cloud_cover
|
||||
if cloud_cover is not None:
|
||||
cloud_cover /= 100
|
||||
return {
|
||||
ATTR_CLOUD_COVER: cloud_cover,
|
||||
ATTR_WIND_GUST: wind_gust,
|
||||
|
@ -26,6 +26,7 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util.color import color_hs_to_xy
|
||||
|
||||
from .const import (
|
||||
COVER_TYPES,
|
||||
@ -189,6 +190,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
|
||||
data["ct"] = kwargs[ATTR_COLOR_TEMP]
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
if COLOR_MODE_XY in self._attr_supported_color_modes:
|
||||
data["xy"] = color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
|
||||
else:
|
||||
data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
|
||||
data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
|
||||
|
||||
|
@ -3,13 +3,17 @@
|
||||
"name": "deCONZ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||
"requirements": ["pydeconz==80"],
|
||||
"requirements": [
|
||||
"pydeconz==81"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@Kane610"],
|
||||
"codeowners": [
|
||||
"@Kane610"
|
||||
],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_push"
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==5.0.0"],
|
||||
"requirements": ["aioesphomeapi==5.0.1"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
"after_dependencies": ["zeroconf", "tag"],
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "FireServiceRota",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
||||
"requirements": ["pyfireservicerota==0.0.40"],
|
||||
"requirements": ["pyfireservicerota==0.0.42"],
|
||||
"codeowners": ["@cyberjunky"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==0.4.2"],
|
||||
"requirements": ["aiohomekit==0.5.1"],
|
||||
"zeroconf": ["_hap._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@Jc2k", "@bdraco"],
|
||||
|
@ -44,7 +44,8 @@ SIMPLE_SENSOR = {
|
||||
"unit": TEMP_CELSIUS,
|
||||
# This sensor is only for temperature characteristics that are not part
|
||||
# of a temperature sensor service.
|
||||
"probe": lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR,
|
||||
"probe": lambda char: char.service.type
|
||||
!= ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,12 @@
|
||||
"domain": "insteon",
|
||||
"name": "Insteon",
|
||||
"documentation": "https://www.home-assistant.io/integrations/insteon",
|
||||
"requirements": ["pyinsteon==1.0.9"],
|
||||
"codeowners": ["@teharris1"],
|
||||
"requirements": [
|
||||
"pyinsteon==1.0.11"
|
||||
],
|
||||
"codeowners": [
|
||||
"@teharris1"
|
||||
],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
}
|
@ -50,7 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
region = entry.data[CONF_REGION]
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
mazda_client = MazdaAPI(email, password, region, websession)
|
||||
mazda_client = MazdaAPI(
|
||||
email, password, region, websession=websession, use_cached_vehicle_list=True
|
||||
)
|
||||
|
||||
try:
|
||||
await mazda_client.validate_credentials()
|
||||
@ -166,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=60),
|
||||
update_interval=timedelta(seconds=180),
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.1.6"],
|
||||
"requirements": ["pymazda==0.2.0"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
@ -77,9 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
neato_session = api.ConfigEntryAuth(hass, entry, session)
|
||||
neato_session = api.ConfigEntryAuth(hass, entry, implementation)
|
||||
hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session
|
||||
hub = NeatoHub(hass, Account(neato_session))
|
||||
|
||||
|
@ -203,7 +203,10 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Dehumidify target."""
|
||||
if self._thermostat.has_dehumidify_support():
|
||||
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
|
||||
else:
|
||||
self._thermostat.set_humidify_setpoint(humidity / 100.0)
|
||||
self._signal_thermostat_update()
|
||||
|
||||
@property
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Config flow for Nexia integration."""
|
||||
import logging
|
||||
|
||||
from nexia.const import BRAND_ASAIR, BRAND_NEXIA
|
||||
from nexia.const import BRAND_ASAIR, BRAND_NEXIA, BRAND_TRANE
|
||||
from nexia.home import NexiaHome
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
@ -9,7 +9,13 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import BRAND_ASAIR_NAME, BRAND_NEXIA_NAME, CONF_BRAND, DOMAIN
|
||||
from .const import (
|
||||
BRAND_ASAIR_NAME,
|
||||
BRAND_NEXIA_NAME,
|
||||
BRAND_TRANE_NAME,
|
||||
CONF_BRAND,
|
||||
DOMAIN,
|
||||
)
|
||||
from .util import is_invalid_auth_code
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -19,7 +25,11 @@ DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_BRAND, default=BRAND_NEXIA): vol.In(
|
||||
{BRAND_NEXIA: BRAND_NEXIA_NAME, BRAND_ASAIR: BRAND_ASAIR_NAME}
|
||||
{
|
||||
BRAND_NEXIA: BRAND_NEXIA_NAME,
|
||||
BRAND_ASAIR: BRAND_ASAIR_NAME,
|
||||
BRAND_TRANE: BRAND_TRANE_NAME,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
@ -31,7 +41,9 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
state_file = hass.config.path(f"nexia_config_{data[CONF_USERNAME]}.conf")
|
||||
state_file = hass.config.path(
|
||||
f"{data[CONF_BRAND]}_config_{data[CONF_USERNAME]}.conf"
|
||||
)
|
||||
try:
|
||||
nexia_home = NexiaHome(
|
||||
username=data[CONF_USERNAME],
|
||||
|
@ -33,4 +33,5 @@ SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE"
|
||||
SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE"
|
||||
|
||||
BRAND_NEXIA_NAME = "Nexia"
|
||||
BRAND_ASAIR_NAME = "American Standard"
|
||||
BRAND_ASAIR_NAME = "American Standard Home"
|
||||
BRAND_TRANE_NAME = "Trane Home"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "nexia",
|
||||
"name": "Nexia/American Standard",
|
||||
"requirements": ["nexia==0.9.7"],
|
||||
"name": "Nexia/American Standard/Trane",
|
||||
"requirements": ["nexia==0.9.9"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"config_flow": true,
|
||||
|
@ -7,4 +7,9 @@ DOMAIN = "recorder"
|
||||
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
|
||||
|
||||
# The maximum number of rows (events) we purge in one delete statement
|
||||
MAX_ROWS_TO_PURGE = 1000
|
||||
|
||||
# sqlite3 has a limit of 999 until version 3.32.0
|
||||
# in https://github.com/sqlite/sqlite/commit/efdba1a8b3c6c967e7fae9c1989c40d420ce64cc
|
||||
# We can increase this back to 1000 once most
|
||||
# have upgraded their sqlite version
|
||||
MAX_ROWS_TO_PURGE = 998
|
||||
|
@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s
|
||||
from .alarms import SonosAlarms
|
||||
from .const import (
|
||||
DATA_SONOS,
|
||||
DATA_SONOS_DISCOVERY_MANAGER,
|
||||
DISCOVERY_INTERVAL,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
@ -46,6 +47,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ADVERTISE_ADDR = "advertise_addr"
|
||||
CONF_INTERFACE_ADDR = "interface_addr"
|
||||
DISCOVERY_IGNORED_MODELS = ["Sonos Boost"]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@ -90,7 +92,7 @@ class SonosData:
|
||||
self.alarms: dict[str, SonosAlarms] = {}
|
||||
self.topology_condition = asyncio.Condition()
|
||||
self.hosts_heartbeat = None
|
||||
self.ssdp_known: set[str] = set()
|
||||
self.discovery_known: set[str] = set()
|
||||
self.boot_counts: dict[str, int] = {}
|
||||
|
||||
|
||||
@ -110,9 +112,7 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry( # noqa: C901
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Sonos from a config entry."""
|
||||
pysonos.config.EVENTS_MODULE = events_asyncio
|
||||
|
||||
@ -122,7 +122,6 @@ async def async_setup_entry( # noqa: C901
|
||||
data = hass.data[DATA_SONOS]
|
||||
config = hass.data[DOMAIN].get("media_player", {})
|
||||
hosts = config.get(CONF_HOSTS, [])
|
||||
discovery_lock = asyncio.Lock()
|
||||
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
|
||||
|
||||
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
|
||||
@ -136,37 +135,12 @@ async def async_setup_entry( # noqa: C901
|
||||
deprecated_address,
|
||||
)
|
||||
|
||||
async def _async_stop_event_listener(event: Event) -> None:
|
||||
await asyncio.gather(
|
||||
*[speaker.async_unsubscribe() for speaker in data.discovered.values()],
|
||||
return_exceptions=True,
|
||||
manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager(
|
||||
hass, entry, data, hosts
|
||||
)
|
||||
if events_asyncio.event_listener:
|
||||
await events_asyncio.event_listener.async_stop()
|
||||
hass.async_create_task(manager.setup_platforms_and_discovery())
|
||||
return True
|
||||
|
||||
def _stop_manual_heartbeat(event: Event) -> None:
|
||||
if data.hosts_heartbeat:
|
||||
data.hosts_heartbeat()
|
||||
data.hosts_heartbeat = None
|
||||
|
||||
def _discovered_player(soco: SoCo) -> None:
|
||||
"""Handle a (re)discovered player."""
|
||||
try:
|
||||
speaker_info = soco.get_speaker_info(True)
|
||||
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
||||
speaker = SonosSpeaker(hass, soco, speaker_info)
|
||||
data.discovered[soco.uid] = speaker
|
||||
for coordinator, coord_dict in [
|
||||
(SonosAlarms, data.alarms),
|
||||
(SonosFavorites, data.favorites),
|
||||
]:
|
||||
if soco.household_id not in coord_dict:
|
||||
new_coordinator = coordinator(hass, soco.household_id)
|
||||
new_coordinator.setup(soco)
|
||||
coord_dict[soco.household_id] = new_coordinator
|
||||
speaker.setup()
|
||||
except (OSError, SoCoException):
|
||||
_LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
|
||||
|
||||
def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None:
|
||||
"""Create a soco instance and return if successful."""
|
||||
@ -182,104 +156,160 @@ async def async_setup_entry( # noqa: C901
|
||||
)
|
||||
return None
|
||||
|
||||
def _manual_hosts(now: datetime.datetime | None = None) -> None:
|
||||
|
||||
class SonosDiscoveryManager:
|
||||
"""Manage sonos discovery."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, data: SonosData, hosts: list[str]
|
||||
) -> None:
|
||||
"""Init discovery manager."""
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.data = data
|
||||
self.hosts = hosts
|
||||
self.discovery_lock = asyncio.Lock()
|
||||
|
||||
async def _async_stop_event_listener(self, event: Event) -> None:
|
||||
await asyncio.gather(
|
||||
*[speaker.async_unsubscribe() for speaker in self.data.discovered.values()],
|
||||
return_exceptions=True,
|
||||
)
|
||||
if events_asyncio.event_listener:
|
||||
await events_asyncio.event_listener.async_stop()
|
||||
|
||||
def _stop_manual_heartbeat(self, event: Event) -> None:
|
||||
if self.data.hosts_heartbeat:
|
||||
self.data.hosts_heartbeat()
|
||||
self.data.hosts_heartbeat = None
|
||||
|
||||
def _discovered_player(self, soco: SoCo) -> None:
|
||||
"""Handle a (re)discovered player."""
|
||||
try:
|
||||
speaker_info = soco.get_speaker_info(True)
|
||||
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
||||
speaker = SonosSpeaker(self.hass, soco, speaker_info)
|
||||
self.data.discovered[soco.uid] = speaker
|
||||
for coordinator, coord_dict in [
|
||||
(SonosAlarms, self.data.alarms),
|
||||
(SonosFavorites, self.data.favorites),
|
||||
]:
|
||||
if soco.household_id not in coord_dict:
|
||||
new_coordinator = coordinator(self.hass, soco.household_id)
|
||||
new_coordinator.setup(soco)
|
||||
coord_dict[soco.household_id] = new_coordinator
|
||||
speaker.setup()
|
||||
except (OSError, SoCoException):
|
||||
_LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
|
||||
|
||||
def _manual_hosts(self, now: datetime.datetime | None = None) -> None:
|
||||
"""Players from network configuration."""
|
||||
for host in hosts:
|
||||
for host in self.hosts:
|
||||
ip_addr = socket.gethostbyname(host)
|
||||
known_uid = next(
|
||||
(
|
||||
uid
|
||||
for uid, speaker in data.discovered.items()
|
||||
for uid, speaker in self.data.discovered.items()
|
||||
if speaker.soco.ip_address == ip_addr
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if known_uid:
|
||||
dispatcher_send(hass, f"{SONOS_SEEN}-{known_uid}")
|
||||
dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}")
|
||||
else:
|
||||
soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED)
|
||||
if soco and soco.is_visible:
|
||||
_discovered_player(soco)
|
||||
self._discovered_player(soco)
|
||||
|
||||
data.hosts_heartbeat = hass.helpers.event.call_later(
|
||||
DISCOVERY_INTERVAL.total_seconds(), _manual_hosts
|
||||
self.data.hosts_heartbeat = self.hass.helpers.event.call_later(
|
||||
DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_signal_update_groups(event):
|
||||
async_dispatcher_send(hass, SONOS_GROUP_UPDATE)
|
||||
def _async_signal_update_groups(self, _event):
|
||||
async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE)
|
||||
|
||||
def _discovered_ip(ip_address):
|
||||
def _discovered_ip(self, ip_address):
|
||||
soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED)
|
||||
if soco and soco.is_visible:
|
||||
_discovered_player(soco)
|
||||
self._discovered_player(soco)
|
||||
|
||||
async def _async_create_discovered_player(uid, discovered_ip, boot_seqnum):
|
||||
async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum):
|
||||
"""Only create one player at a time."""
|
||||
async with discovery_lock:
|
||||
if uid not in data.discovered:
|
||||
await hass.async_add_executor_job(_discovered_ip, discovered_ip)
|
||||
async with self.discovery_lock:
|
||||
if uid not in self.data.discovered:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._discovered_ip, discovered_ip
|
||||
)
|
||||
return
|
||||
|
||||
if boot_seqnum and boot_seqnum > data.boot_counts[uid]:
|
||||
data.boot_counts[uid] = boot_seqnum
|
||||
if soco := await hass.async_add_executor_job(
|
||||
if boot_seqnum and boot_seqnum > self.data.boot_counts[uid]:
|
||||
self.data.boot_counts[uid] = boot_seqnum
|
||||
if soco := await self.hass.async_add_executor_job(
|
||||
_create_soco, discovered_ip, SoCoCreationSource.REBOOTED
|
||||
):
|
||||
async_dispatcher_send(hass, f"{SONOS_REBOOTED}-{uid}", soco)
|
||||
async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}", soco)
|
||||
else:
|
||||
async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}")
|
||||
async_dispatcher_send(self.hass, f"{SONOS_SEEN}-{uid}")
|
||||
|
||||
@callback
|
||||
def _async_discovered_player(info):
|
||||
def _async_ssdp_discovered_player(self, info):
|
||||
discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname
|
||||
boot_seqnum = info.get("X-RINCON-BOOTSEQ")
|
||||
uid = info.get(ssdp.ATTR_UPNP_UDN)
|
||||
if uid.startswith("uuid:"):
|
||||
uid = uid[5:]
|
||||
if boot_seqnum := info.get("X-RINCON-BOOTSEQ"):
|
||||
boot_seqnum = int(boot_seqnum)
|
||||
data.boot_counts.setdefault(uid, boot_seqnum)
|
||||
if uid not in data.ssdp_known:
|
||||
_LOGGER.debug("New discovery: %s", info)
|
||||
data.ssdp_known.add(uid)
|
||||
discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname
|
||||
asyncio.create_task(
|
||||
_async_create_discovered_player(uid, discovered_ip, boot_seqnum)
|
||||
self.async_discovered_player(
|
||||
info, discovered_ip, uid, boot_seqnum, info.get("modelName")
|
||||
)
|
||||
|
||||
async def setup_platforms_and_discovery():
|
||||
@callback
|
||||
def async_discovered_player(self, info, discovered_ip, uid, boot_seqnum, model):
|
||||
"""Handle discovery via ssdp or zeroconf."""
|
||||
if model in DISCOVERY_IGNORED_MODELS:
|
||||
_LOGGER.debug("Ignoring device: %s", info)
|
||||
return
|
||||
if boot_seqnum:
|
||||
boot_seqnum = int(boot_seqnum)
|
||||
self.data.boot_counts.setdefault(uid, boot_seqnum)
|
||||
if uid not in self.data.discovery_known:
|
||||
_LOGGER.debug("New discovery uid=%s: %s", uid, info)
|
||||
self.data.discovery_known.add(uid)
|
||||
asyncio.create_task(
|
||||
self._async_create_discovered_player(uid, discovered_ip, boot_seqnum)
|
||||
)
|
||||
|
||||
async def setup_platforms_and_discovery(self):
|
||||
"""Set up platforms and discovery."""
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
self.hass.config_entries.async_forward_entry_setup(self.entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, _async_signal_update_groups
|
||||
self.entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, self._async_signal_update_groups
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener
|
||||
self.entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener
|
||||
)
|
||||
)
|
||||
_LOGGER.debug("Adding discovery job")
|
||||
if hosts:
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat
|
||||
if self.hosts:
|
||||
self.entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat
|
||||
)
|
||||
)
|
||||
await hass.async_add_executor_job(_manual_hosts)
|
||||
await self.hass.async_add_executor_job(self._manual_hosts)
|
||||
return
|
||||
|
||||
entry.async_on_unload(
|
||||
self.entry.async_on_unload(
|
||||
ssdp.async_register_callback(
|
||||
hass, _async_discovered_player, {"st": UPNP_ST}
|
||||
self.hass, self._async_ssdp_discovered_player, {"st": UPNP_ST}
|
||||
)
|
||||
)
|
||||
|
||||
hass.async_create_task(setup_platforms_and_discovery())
|
||||
|
||||
return True
|
||||
|
@ -1,10 +1,19 @@
|
||||
"""Config flow for SONOS."""
|
||||
import logging
|
||||
|
||||
import pysonos
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN
|
||||
from .helpers import hostname_to_uid
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
@ -13,4 +22,37 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
return bool(result)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(DOMAIN, "Sonos", _async_has_devices)
|
||||
class SonosDiscoveryFlowHandler(DiscoveryFlowHandler):
|
||||
"""Sonos discovery flow that callsback zeroconf updates."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Init discovery flow."""
|
||||
super().__init__(DOMAIN, "Sonos", _async_has_devices)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by zeroconf."""
|
||||
hostname = discovery_info["hostname"]
|
||||
if hostname is None or not hostname.startswith("Sonos-"):
|
||||
return self.async_abort(reason="not_sonos_device")
|
||||
await self.async_set_unique_id(self._domain, raise_on_progress=False)
|
||||
host = discovery_info[CONF_HOST]
|
||||
properties = discovery_info["properties"]
|
||||
boot_seqnum = properties.get("bootseq")
|
||||
model = properties.get("model")
|
||||
uid = hostname_to_uid(hostname)
|
||||
_LOGGER.debug(
|
||||
"Calling async_discovered_player for %s with uid=%s and boot_seqnum=%s",
|
||||
host,
|
||||
uid,
|
||||
boot_seqnum,
|
||||
)
|
||||
if discovery_manager := self.hass.data.get(DATA_SONOS_DISCOVERY_MANAGER):
|
||||
discovery_manager.async_discovered_player(
|
||||
properties, host, uid, boot_seqnum, model
|
||||
)
|
||||
return await self.async_step_discovery(discovery_info)
|
||||
|
||||
|
||||
config_entries.HANDLERS.register(DOMAIN)(SonosDiscoveryFlowHandler)
|
||||
|
@ -26,6 +26,7 @@ UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
|
||||
DOMAIN = "sonos"
|
||||
DATA_SONOS = "sonos_media_player"
|
||||
DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager"
|
||||
PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN}
|
||||
|
||||
SONOS_ARTIST = "artists"
|
||||
@ -154,3 +155,5 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
DISCOVERY_INTERVAL = datetime.timedelta(seconds=60)
|
||||
SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL
|
||||
SUBSCRIPTION_TIMEOUT = 1200
|
||||
|
||||
MDNS_SERVICE = "_sonos._tcp.local."
|
||||
|
@ -9,6 +9,9 @@ from pysonos.exceptions import SoCoException, SoCoUPnPException
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
UID_PREFIX = "RINCON_"
|
||||
UID_POSTFIX = "01400"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -36,3 +39,19 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def uid_to_short_hostname(uid: str) -> str:
|
||||
"""Convert a Sonos uid to a short hostname."""
|
||||
hostname_uid = uid
|
||||
if hostname_uid.startswith(UID_PREFIX):
|
||||
hostname_uid = hostname_uid[len(UID_PREFIX) :]
|
||||
if hostname_uid.endswith(UID_POSTFIX):
|
||||
hostname_uid = hostname_uid[: -len(UID_POSTFIX)]
|
||||
return f"Sonos-{hostname_uid}"
|
||||
|
||||
|
||||
def hostname_to_uid(hostname: str) -> str:
|
||||
"""Convert a Sonos hostname to a uid."""
|
||||
baseuid = hostname.split("-")[1].replace(".local.", "")
|
||||
return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}"
|
||||
|
@ -3,9 +3,10 @@
|
||||
"name": "Sonos",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"requirements": ["pysonos==0.0.51"],
|
||||
"requirements": ["pysonos==0.0.52"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["plex"],
|
||||
"after_dependencies": ["plex", "zeroconf"],
|
||||
"zeroconf": ["_sonos._tcp.local."],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
|
@ -19,6 +19,7 @@ from pysonos.music_library import MusicLibrary
|
||||
from pysonos.plugins.sharelink import ShareLinkPlugin
|
||||
from pysonos.snapshot import Snapshot
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
@ -37,6 +38,7 @@ from .const import (
|
||||
BATTERY_SCAN_INTERVAL,
|
||||
DATA_SONOS,
|
||||
DOMAIN,
|
||||
MDNS_SERVICE,
|
||||
PLATFORMS,
|
||||
SCAN_INTERVAL,
|
||||
SEEN_EXPIRE_TIME,
|
||||
@ -56,7 +58,7 @@ from .const import (
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
)
|
||||
from .favorites import SonosFavorites
|
||||
from .helpers import soco_error
|
||||
from .helpers import soco_error, uid_to_short_hostname
|
||||
|
||||
EVENT_CHARGING = {
|
||||
"CHARGING": True,
|
||||
@ -498,12 +500,27 @@ class SonosSpeaker:
|
||||
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
||||
) -> None:
|
||||
"""Make this player unavailable when it was not seen recently."""
|
||||
self._share_link_plugin = None
|
||||
|
||||
if self._seen_timer:
|
||||
self._seen_timer()
|
||||
self._seen_timer = None
|
||||
|
||||
hostname = uid_to_short_hostname(self.soco.uid)
|
||||
zcname = f"{hostname}.{MDNS_SERVICE}"
|
||||
aiozeroconf = await zeroconf.async_get_async_instance(self.hass)
|
||||
if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname):
|
||||
# We can still see the speaker via zeroconf check again later.
|
||||
self._seen_timer = self.hass.helpers.event.async_call_later(
|
||||
SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"No activity and could not locate %s on the network. Marking unavailable",
|
||||
zcname,
|
||||
)
|
||||
|
||||
self._share_link_plugin = None
|
||||
|
||||
if self._poll_timer:
|
||||
self._poll_timer()
|
||||
self._poll_timer = None
|
||||
@ -511,7 +528,7 @@ class SonosSpeaker:
|
||||
await self.async_unsubscribe()
|
||||
|
||||
if not will_reconnect:
|
||||
self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid)
|
||||
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
|
||||
self.async_write_entity_states()
|
||||
|
||||
async def async_rebooted(self, soco: SoCo) -> None:
|
||||
|
@ -6,6 +6,7 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"not_sonos_device": "Discovered device is not a Sonos device",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"not_sonos_device": "Discovered device is not a Sonos device",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"step": {
|
||||
|
@ -29,6 +29,8 @@ from .flow import FlowDispatcher, SSDPFlow
|
||||
DOMAIN = "ssdp"
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
IPV4_BROADCAST = IPv4Address("255.255.255.255")
|
||||
|
||||
# Attributes for accessing info from SSDP response
|
||||
ATTR_SSDP_LOCATION = "ssdp_location"
|
||||
ATTR_SSDP_ST = "ssdp_st"
|
||||
@ -236,7 +238,20 @@ class Scanner:
|
||||
async_callback=self._async_process_entry, source_ip=source_ip
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
IPv4Address(source_ip)
|
||||
except ValueError:
|
||||
continue
|
||||
# Some sonos devices only seem to respond if we send to the broadcast
|
||||
# address. This matches pysonos' behavior
|
||||
# https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120
|
||||
self._ssdp_listeners.append(
|
||||
SSDPListener(
|
||||
async_callback=self._async_process_entry,
|
||||
source_ip=source_ip,
|
||||
target_ip=IPV4_BROADCAST,
|
||||
)
|
||||
)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start
|
||||
|
@ -158,10 +158,10 @@ class SurePetcareAPI:
|
||||
# https://github.com/PyCQA/pylint/issues/2062
|
||||
# pylint: disable=no-member
|
||||
if state == LockState.UNLOCKED.name.lower():
|
||||
await self.surepy.unlock(flap_id)
|
||||
await self.surepy.sac.unlock(flap_id)
|
||||
elif state == LockState.LOCKED_IN.name.lower():
|
||||
await self.surepy.lock_in(flap_id)
|
||||
await self.surepy.sac.lock_in(flap_id)
|
||||
elif state == LockState.LOCKED_OUT.name.lower():
|
||||
await self.surepy.lock_out(flap_id)
|
||||
await self.surepy.sac.lock_out(flap_id)
|
||||
elif state == LockState.LOCKED_ALL.name.lower():
|
||||
await self.surepy.lock(flap_id)
|
||||
await self.surepy.sac.lock(flap_id)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Version",
|
||||
"documentation": "https://www.home-assistant.io/integrations/version",
|
||||
"requirements": [
|
||||
"pyhaversion==21.5.0"
|
||||
"pyhaversion==21.7.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@fabaff",
|
||||
|
@ -10,7 +10,7 @@
|
||||
"zha-quirks==0.0.59",
|
||||
"zigpy-cc==0.5.2",
|
||||
"zigpy-deconz==0.12.0",
|
||||
"zigpy==0.35.1",
|
||||
"zigpy==0.35.2",
|
||||
"zigpy-xbee==0.13.0",
|
||||
"zigpy-zigate==0.7.3",
|
||||
"zigpy-znp==0.5.1"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.27.0"],
|
||||
"requirements": ["zwave-js-server-python==0.27.1"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["http", "websocket_api"],
|
||||
"iot_class": "local_push"
|
||||
|
@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__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)
|
||||
|
@ -186,6 +186,11 @@ ZEROCONF = {
|
||||
"name": "brother*"
|
||||
}
|
||||
],
|
||||
"_sonos._tcp.local.": [
|
||||
{
|
||||
"domain": "sonos"
|
||||
}
|
||||
],
|
||||
"_spotify-connect._tcp.local.": [
|
||||
{
|
||||
"domain": "spotify"
|
||||
|
@ -160,7 +160,7 @@ aioeafm==0.1.2
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==5.0.0
|
||||
aioesphomeapi==5.0.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==0.4.1
|
||||
@ -175,7 +175,7 @@ aioguardian==1.0.4
|
||||
aioharmony==0.2.7
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.4.2
|
||||
aiohomekit==0.5.1
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -290,7 +290,7 @@ aprslib==0.6.46
|
||||
aqualogic==2.6
|
||||
|
||||
# homeassistant.components.arcam_fmj
|
||||
arcam-fmj==0.5.3
|
||||
arcam-fmj==0.7.0
|
||||
|
||||
# homeassistant.components.arris_tg2492lg
|
||||
arris-tg2492lg==1.1.0
|
||||
@ -1020,7 +1020,7 @@ nettigo-air-monitor==1.0.0
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.7
|
||||
nexia==0.9.9
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.1.0
|
||||
@ -1375,7 +1375,7 @@ pydaikin==2.4.4
|
||||
pydanfossair==0.1.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==80
|
||||
pydeconz==81
|
||||
|
||||
# homeassistant.components.delijn
|
||||
pydelijn==0.6.1
|
||||
@ -1423,7 +1423,7 @@ pyezviz==0.1.8.9
|
||||
pyfido==2.1.1
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.40
|
||||
pyfireservicerota==0.0.42
|
||||
|
||||
# homeassistant.components.flexit
|
||||
pyflexit==0.3
|
||||
@ -1466,7 +1466,7 @@ pygtfs==0.1.6
|
||||
pygti==0.9.2
|
||||
|
||||
# homeassistant.components.version
|
||||
pyhaversion==21.5.0
|
||||
pyhaversion==21.7.0
|
||||
|
||||
# homeassistant.components.heos
|
||||
pyheos==0.7.2
|
||||
@ -1490,7 +1490,7 @@ pyialarm==1.9.0
|
||||
pyicloud==0.10.2
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.0.9
|
||||
pyinsteon==1.0.11
|
||||
|
||||
# homeassistant.components.intesishome
|
||||
pyintesishome==1.7.6
|
||||
@ -1571,7 +1571,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.1.6
|
||||
pymazda==0.2.0
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
@ -1773,7 +1773,7 @@ pysnmp==4.4.12
|
||||
pysoma==0.0.10
|
||||
|
||||
# homeassistant.components.sonos
|
||||
pysonos==0.0.51
|
||||
pysonos==0.0.52
|
||||
|
||||
# homeassistant.components.spc
|
||||
pyspcwebgw==0.4.0
|
||||
@ -2451,10 +2451,10 @@ zigpy-zigate==0.7.3
|
||||
zigpy-znp==0.5.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.35.1
|
||||
zigpy==0.35.2
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.27.0
|
||||
zwave-js-server-python==0.27.1
|
||||
|
@ -100,7 +100,7 @@ aioeafm==0.1.2
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==5.0.0
|
||||
aioesphomeapi==5.0.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==0.4.1
|
||||
@ -112,7 +112,7 @@ aioguardian==1.0.4
|
||||
aioharmony==0.2.7
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.4.2
|
||||
aiohomekit==0.5.1
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -191,7 +191,7 @@ apprise==0.9.3
|
||||
aprslib==0.6.46
|
||||
|
||||
# homeassistant.components.arcam_fmj
|
||||
arcam-fmj==0.5.3
|
||||
arcam-fmj==0.7.0
|
||||
|
||||
# homeassistant.components.dlna_dmr
|
||||
# homeassistant.components.ssdp
|
||||
@ -574,7 +574,7 @@ netdisco==2.9.0
|
||||
nettigo-air-monitor==1.0.0
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.7
|
||||
nexia==0.9.9
|
||||
|
||||
# homeassistant.components.notify_events
|
||||
notify-events==1.0.4
|
||||
@ -770,7 +770,7 @@ pycoolmasternet-async==0.1.2
|
||||
pydaikin==2.4.4
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==80
|
||||
pydeconz==81
|
||||
|
||||
# homeassistant.components.dexcom
|
||||
pydexcom==0.2.0
|
||||
@ -791,7 +791,7 @@ pyezviz==0.1.8.9
|
||||
pyfido==2.1.1
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.40
|
||||
pyfireservicerota==0.0.42
|
||||
|
||||
# homeassistant.components.flume
|
||||
pyflume==0.5.5
|
||||
@ -819,7 +819,7 @@ pygatt[GATTTOOL]==4.0.5
|
||||
pygti==0.9.2
|
||||
|
||||
# homeassistant.components.version
|
||||
pyhaversion==21.5.0
|
||||
pyhaversion==21.7.0
|
||||
|
||||
# homeassistant.components.heos
|
||||
pyheos==0.7.2
|
||||
@ -837,7 +837,7 @@ pyialarm==1.9.0
|
||||
pyicloud==0.10.2
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.0.9
|
||||
pyinsteon==1.0.11
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
@ -888,7 +888,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.1.6
|
||||
pymazda==0.2.0
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.3
|
||||
@ -1006,7 +1006,7 @@ pysmartthings==0.7.6
|
||||
pysoma==0.0.10
|
||||
|
||||
# homeassistant.components.sonos
|
||||
pysonos==0.0.51
|
||||
pysonos==0.0.52
|
||||
|
||||
# homeassistant.components.spc
|
||||
pyspcwebgw==0.4.0
|
||||
@ -1345,7 +1345,7 @@ zigpy-zigate==0.7.3
|
||||
zigpy-znp==0.5.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.35.1
|
||||
zigpy==0.35.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.27.0
|
||||
zwave-js-server-python==0.27.1
|
||||
|
@ -182,7 +182,7 @@ async def test_v4_sensor(
|
||||
check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688")
|
||||
check_sensor_state(hass, GHI, "0.0")
|
||||
check_sensor_state(hass, CLOUD_BASE, "1.1909")
|
||||
check_sensor_state(hass, CLOUD_COVER, "1.0")
|
||||
check_sensor_state(hass, CLOUD_COVER, "100")
|
||||
check_sensor_state(hass, CLOUD_CEILING, "1.1909")
|
||||
check_sensor_state(hass, WIND_GUST, "5.6506")
|
||||
check_sensor_state(hass, PRECIPITATION_TYPE, "rain")
|
||||
|
@ -228,7 +228,7 @@ async def test_v3_weather(
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 1
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 100
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758
|
||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
||||
|
||||
@ -391,6 +391,6 @@ async def test_v4_weather(
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 1
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 100
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421
|
||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
||||
|
@ -371,6 +371,34 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket):
|
||||
@pytest.mark.parametrize(
|
||||
"input,expected",
|
||||
[
|
||||
( # Turn on light with hue and sat
|
||||
{
|
||||
"light_on": True,
|
||||
"service": SERVICE_TURN_ON,
|
||||
"call": {
|
||||
ATTR_ENTITY_ID: "light.hue_go",
|
||||
ATTR_HS_COLOR: (20, 30),
|
||||
},
|
||||
},
|
||||
{
|
||||
"on": True,
|
||||
"xy": (0.411, 0.351),
|
||||
},
|
||||
),
|
||||
( # Turn on light with XY color
|
||||
{
|
||||
"light_on": True,
|
||||
"service": SERVICE_TURN_ON,
|
||||
"call": {
|
||||
ATTR_ENTITY_ID: "light.hue_go",
|
||||
ATTR_XY_COLOR: (0.411, 0.351),
|
||||
},
|
||||
},
|
||||
{
|
||||
"on": True,
|
||||
"xy": (0.411, 0.351),
|
||||
},
|
||||
),
|
||||
( # Turn on light with short color loop
|
||||
{
|
||||
"light_on": False,
|
||||
@ -811,9 +839,8 @@ async def test_groups(hass, aioclient_mock, input, expected):
|
||||
},
|
||||
},
|
||||
{
|
||||
"hue": 45510,
|
||||
"on": True,
|
||||
"sat": 127,
|
||||
"xy": (0.235, 0.164),
|
||||
},
|
||||
),
|
||||
( # Turn on group with short color loop
|
||||
@ -827,9 +854,8 @@ async def test_groups(hass, aioclient_mock, input, expected):
|
||||
},
|
||||
},
|
||||
{
|
||||
"hue": 45510,
|
||||
"on": True,
|
||||
"sat": 127,
|
||||
"xy": (0.235, 0.164),
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -86,6 +86,16 @@ async def test_temperature_sensor_read_state(hass, utcnow):
|
||||
assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE
|
||||
|
||||
|
||||
async def test_temperature_sensor_not_added_twice(hass, utcnow):
|
||||
"""A standalone temperature sensor should not get a characteristic AND a service entity."""
|
||||
helper = await setup_test_component(
|
||||
hass, create_temperature_sensor_service, suffix="temperature"
|
||||
)
|
||||
|
||||
for state in hass.states.async_all():
|
||||
assert state.entity_id == helper.entity_id
|
||||
|
||||
|
||||
async def test_humidity_sensor_read_state(hass, utcnow):
|
||||
"""Test reading the state of a HomeKit humidity sensor accessory."""
|
||||
helper = await setup_test_component(
|
||||
|
@ -97,7 +97,7 @@ async def test_update_auth_failure(hass: HomeAssistant):
|
||||
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||
side_effect=MazdaAuthenticationException("Login failed"),
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
@ -136,7 +136,7 @@ async def test_update_general_failure(hass: HomeAssistant):
|
||||
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||
side_effect=Exception("Unknown exception"),
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
|
||||
|
92
tests/components/sonos/test_config_flow.py
Normal file
92
tests/components/sonos/test_config_flow.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Test the sonos config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant import config_entries, core, setup
|
||||
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN
|
||||
|
||||
|
||||
@patch("homeassistant.components.sonos.config_flow.pysonos.discover", return_value=True)
|
||||
async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant):
|
||||
"""Test we get the user initiated form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
with patch(
|
||||
"homeassistant.components.sonos.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.sonos.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Sonos"
|
||||
assert result2["data"] == {}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_zeroconf_form(hass: core.HomeAssistant):
|
||||
"""Test we pass sonos devices to the discovery manager."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"host": "192.168.4.2",
|
||||
"hostname": "Sonos-aaa",
|
||||
"properties": {"bootseq": "1234"},
|
||||
},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sonos.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.sonos.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Sonos"
|
||||
assert result2["data"] == {}
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert len(mock_manager.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant):
|
||||
"""Test we abort on non-sonos devices."""
|
||||
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"host": "192.168.4.2",
|
||||
"hostname": "not-aaa",
|
||||
"properties": {"bootseq": "1234"},
|
||||
},
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_sonos_device"
|
||||
assert len(mock_manager.mock_calls) == 0
|
17
tests/components/sonos/test_helpers.py
Normal file
17
tests/components/sonos/test_helpers.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Test the sonos config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sonos.helpers import (
|
||||
hostname_to_uid,
|
||||
uid_to_short_hostname,
|
||||
)
|
||||
|
||||
|
||||
async def test_uid_to_short_hostname():
|
||||
"""Test we can convert a uid to a short hostname."""
|
||||
assert uid_to_short_hostname("RINCON_347E5C0CF1E301400") == "Sonos-347E5C0CF1E3"
|
||||
|
||||
|
||||
async def test_uid_to_hostname():
|
||||
"""Test we can convert a hostname to a uid."""
|
||||
assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400"
|
@ -295,15 +295,15 @@ async def test_start_stop_scanner(async_start_mock, async_search_mock, hass):
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
assert async_start_mock.call_count == 1
|
||||
assert async_search_mock.call_count == 1
|
||||
assert async_start_mock.call_count == 2
|
||||
assert async_search_mock.call_count == 2
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
assert async_start_mock.call_count == 1
|
||||
assert async_search_mock.call_count == 1
|
||||
assert async_start_mock.call_count == 2
|
||||
assert async_search_mock.call_count == 2
|
||||
|
||||
|
||||
async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog):
|
||||
@ -459,11 +459,11 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog):
|
||||
await hass.async_block_till_done()
|
||||
assert hass.state == CoreState.running
|
||||
|
||||
assert len(integration_callbacks) == 3
|
||||
assert len(integration_callbacks_from_cache) == 3
|
||||
assert len(integration_match_all_callbacks) == 3
|
||||
assert len(integration_callbacks) == 5
|
||||
assert len(integration_callbacks_from_cache) == 5
|
||||
assert len(integration_match_all_callbacks) == 5
|
||||
assert len(integration_match_all_not_present_callbacks) == 0
|
||||
assert len(match_any_callbacks) == 3
|
||||
assert len(match_any_callbacks) == 5
|
||||
assert len(not_matching_integration_callbacks) == 0
|
||||
assert integration_callbacks[0] == {
|
||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
||||
@ -546,7 +546,7 @@ async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog
|
||||
assert hass.state == CoreState.running
|
||||
|
||||
assert (
|
||||
len(integration_callbacks) == 2
|
||||
len(integration_callbacks) == 4
|
||||
) # unsolicited callbacks without st are not cached
|
||||
assert integration_callbacks[0] == {
|
||||
"UDN": "uuid:RINCON_1111BB963FD801400",
|
||||
@ -635,7 +635,7 @@ async def test_scan_second_hit(hass, aioclient_mock, caplog):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(integration_callbacks) == 2
|
||||
assert len(integration_callbacks) == 4
|
||||
assert integration_callbacks[0] == {
|
||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
||||
ssdp.ATTR_SSDP_EXT: "",
|
||||
@ -781,7 +781,12 @@ async def test_async_detect_interfaces_setting_empty_route(hass):
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert {create_args[0][1]["source_ip"], create_args[1][1]["source_ip"]} == {
|
||||
IPv4Address("192.168.1.5"),
|
||||
IPv6Address("2001:db8::"),
|
||||
argset = set()
|
||||
for argmap in create_args:
|
||||
argset.add((argmap[1].get("source_ip"), argmap[1].get("target_ip")))
|
||||
|
||||
assert argset == {
|
||||
(IPv6Address("2001:db8::"), None),
|
||||
(IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")),
|
||||
(IPv4Address("192.168.1.5"), None),
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user