mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +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/netgear_lte/*
|
||||||
homeassistant/components/netio/switch.py
|
homeassistant/components/netio/switch.py
|
||||||
homeassistant/components/neurio_energy/sensor.py
|
homeassistant/components/neurio_energy/sensor.py
|
||||||
|
homeassistant/components/nexia/climate.py
|
||||||
homeassistant/components/nextcloud/*
|
homeassistant/components/nextcloud/*
|
||||||
homeassistant/components/nfandroidtv/notify.py
|
homeassistant/components/nfandroidtv/notify.py
|
||||||
homeassistant/components/niko_home_control/light.py
|
homeassistant/components/niko_home_control/light.py
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Arcam FMJ Receivers",
|
"name": "Arcam FMJ Receivers",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||||
"requirements": ["arcam-fmj==0.5.3"],
|
"requirements": ["arcam-fmj==0.7.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||||
|
@ -214,7 +214,6 @@ CC_SENSOR_TYPES = [
|
|||||||
ATTR_FIELD: CC_ATTR_CLOUD_COVER,
|
ATTR_FIELD: CC_ATTR_CLOUD_COVER,
|
||||||
ATTR_NAME: "Cloud Cover",
|
ATTR_NAME: "Cloud Cover",
|
||||||
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
|
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
|
||||||
ATTR_SCALE: 1 / 100,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ATTR_FIELD: CC_ATTR_WIND_GUST,
|
ATTR_FIELD: CC_ATTR_WIND_GUST,
|
||||||
|
@ -207,8 +207,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
|||||||
distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||||
)
|
)
|
||||||
cloud_cover = self.cloud_cover
|
cloud_cover = self.cloud_cover
|
||||||
if cloud_cover is not None:
|
|
||||||
cloud_cover /= 100
|
|
||||||
return {
|
return {
|
||||||
ATTR_CLOUD_COVER: cloud_cover,
|
ATTR_CLOUD_COVER: cloud_cover,
|
||||||
ATTR_WIND_GUST: wind_gust,
|
ATTR_WIND_GUST: wind_gust,
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.components.light import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.util.color import color_hs_to_xy
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
COVER_TYPES,
|
COVER_TYPES,
|
||||||
@ -189,6 +190,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
|
|||||||
data["ct"] = kwargs[ATTR_COLOR_TEMP]
|
data["ct"] = kwargs[ATTR_COLOR_TEMP]
|
||||||
|
|
||||||
if ATTR_HS_COLOR in kwargs:
|
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["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
|
||||||
data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
|
data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
|
||||||
|
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
"name": "deCONZ",
|
"name": "deCONZ",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||||
"requirements": ["pydeconz==80"],
|
"requirements": [
|
||||||
|
"pydeconz==81"
|
||||||
|
],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics"
|
"manufacturer": "Royal Philips Electronics"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codeowners": ["@Kane610"],
|
"codeowners": [
|
||||||
|
"@Kane610"
|
||||||
|
],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
}
|
}
|
@ -3,7 +3,7 @@
|
|||||||
"name": "ESPHome",
|
"name": "ESPHome",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||||
"requirements": ["aioesphomeapi==5.0.0"],
|
"requirements": ["aioesphomeapi==5.0.1"],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."],
|
"zeroconf": ["_esphomelib._tcp.local."],
|
||||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||||
"after_dependencies": ["zeroconf", "tag"],
|
"after_dependencies": ["zeroconf", "tag"],
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "FireServiceRota",
|
"name": "FireServiceRota",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
||||||
"requirements": ["pyfireservicerota==0.0.40"],
|
"requirements": ["pyfireservicerota==0.0.42"],
|
||||||
"codeowners": ["@cyberjunky"],
|
"codeowners": ["@cyberjunky"],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "HomeKit Controller",
|
"name": "HomeKit Controller",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||||
"requirements": ["aiohomekit==0.4.2"],
|
"requirements": ["aiohomekit==0.5.1"],
|
||||||
"zeroconf": ["_hap._tcp.local."],
|
"zeroconf": ["_hap._tcp.local."],
|
||||||
"after_dependencies": ["zeroconf"],
|
"after_dependencies": ["zeroconf"],
|
||||||
"codeowners": ["@Jc2k", "@bdraco"],
|
"codeowners": ["@Jc2k", "@bdraco"],
|
||||||
|
@ -44,7 +44,8 @@ SIMPLE_SENSOR = {
|
|||||||
"unit": TEMP_CELSIUS,
|
"unit": TEMP_CELSIUS,
|
||||||
# This sensor is only for temperature characteristics that are not part
|
# This sensor is only for temperature characteristics that are not part
|
||||||
# of a temperature sensor service.
|
# 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",
|
"domain": "insteon",
|
||||||
"name": "Insteon",
|
"name": "Insteon",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/insteon",
|
"documentation": "https://www.home-assistant.io/integrations/insteon",
|
||||||
"requirements": ["pyinsteon==1.0.9"],
|
"requirements": [
|
||||||
"codeowners": ["@teharris1"],
|
"pyinsteon==1.0.11"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@teharris1"
|
||||||
|
],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
}
|
}
|
@ -50,7 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
region = entry.data[CONF_REGION]
|
region = entry.data[CONF_REGION]
|
||||||
|
|
||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
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:
|
try:
|
||||||
await mazda_client.validate_credentials()
|
await mazda_client.validate_credentials()
|
||||||
@ -166,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_method=async_update_data,
|
update_method=async_update_data,
|
||||||
update_interval=timedelta(seconds=60),
|
update_interval=timedelta(seconds=180),
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Mazda Connected Services",
|
"name": "Mazda Connected Services",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||||
"requirements": ["pymazda==0.1.6"],
|
"requirements": ["pymazda==0.2.0"],
|
||||||
"codeowners": ["@bdr99"],
|
"codeowners": ["@bdr99"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"iot_class": "cloud_polling"
|
"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, implementation)
|
||||||
|
|
||||||
neato_session = api.ConfigEntryAuth(hass, entry, session)
|
|
||||||
hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session
|
hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session
|
||||||
hub = NeatoHub(hass, Account(neato_session))
|
hub = NeatoHub(hass, Account(neato_session))
|
||||||
|
|
||||||
|
@ -203,7 +203,10 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
|||||||
|
|
||||||
def set_humidity(self, humidity):
|
def set_humidity(self, humidity):
|
||||||
"""Dehumidify target."""
|
"""Dehumidify target."""
|
||||||
|
if self._thermostat.has_dehumidify_support():
|
||||||
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
|
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
|
||||||
|
else:
|
||||||
|
self._thermostat.set_humidify_setpoint(humidity / 100.0)
|
||||||
self._signal_thermostat_update()
|
self._signal_thermostat_update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Config flow for Nexia integration."""
|
"""Config flow for Nexia integration."""
|
||||||
import logging
|
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 nexia.home import NexiaHome
|
||||||
from requests.exceptions import ConnectTimeout, HTTPError
|
from requests.exceptions import ConnectTimeout, HTTPError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -9,7 +9,13 @@ import voluptuous as vol
|
|||||||
from homeassistant import config_entries, core, exceptions
|
from homeassistant import config_entries, core, exceptions
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
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
|
from .util import is_invalid_auth_code
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -19,7 +25,11 @@ DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_USERNAME): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Required(CONF_BRAND, default=BRAND_NEXIA): vol.In(
|
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.
|
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:
|
try:
|
||||||
nexia_home = NexiaHome(
|
nexia_home = NexiaHome(
|
||||||
username=data[CONF_USERNAME],
|
username=data[CONF_USERNAME],
|
||||||
|
@ -33,4 +33,5 @@ SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE"
|
|||||||
SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE"
|
SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE"
|
||||||
|
|
||||||
BRAND_NEXIA_NAME = "Nexia"
|
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",
|
"domain": "nexia",
|
||||||
"name": "Nexia/American Standard",
|
"name": "Nexia/American Standard/Trane",
|
||||||
"requirements": ["nexia==0.9.7"],
|
"requirements": ["nexia==0.9.9"],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
@ -7,4 +7,9 @@ DOMAIN = "recorder"
|
|||||||
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
|
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
|
||||||
|
|
||||||
# The maximum number of rows (events) we purge in one delete statement
|
# 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 .alarms import SonosAlarms
|
||||||
from .const import (
|
from .const import (
|
||||||
DATA_SONOS,
|
DATA_SONOS,
|
||||||
|
DATA_SONOS_DISCOVERY_MANAGER,
|
||||||
DISCOVERY_INTERVAL,
|
DISCOVERY_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
@ -46,6 +47,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONF_ADVERTISE_ADDR = "advertise_addr"
|
CONF_ADVERTISE_ADDR = "advertise_addr"
|
||||||
CONF_INTERFACE_ADDR = "interface_addr"
|
CONF_INTERFACE_ADDR = "interface_addr"
|
||||||
|
DISCOVERY_IGNORED_MODELS = ["Sonos Boost"]
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
@ -90,7 +92,7 @@ class SonosData:
|
|||||||
self.alarms: dict[str, SonosAlarms] = {}
|
self.alarms: dict[str, SonosAlarms] = {}
|
||||||
self.topology_condition = asyncio.Condition()
|
self.topology_condition = asyncio.Condition()
|
||||||
self.hosts_heartbeat = None
|
self.hosts_heartbeat = None
|
||||||
self.ssdp_known: set[str] = set()
|
self.discovery_known: set[str] = set()
|
||||||
self.boot_counts: dict[str, int] = {}
|
self.boot_counts: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
@ -110,9 +112,7 @@ async def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry( # noqa: C901
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: ConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
pysonos.config.EVENTS_MODULE = events_asyncio
|
pysonos.config.EVENTS_MODULE = events_asyncio
|
||||||
|
|
||||||
@ -122,7 +122,6 @@ async def async_setup_entry( # noqa: C901
|
|||||||
data = hass.data[DATA_SONOS]
|
data = hass.data[DATA_SONOS]
|
||||||
config = hass.data[DOMAIN].get("media_player", {})
|
config = hass.data[DOMAIN].get("media_player", {})
|
||||||
hosts = config.get(CONF_HOSTS, [])
|
hosts = config.get(CONF_HOSTS, [])
|
||||||
discovery_lock = asyncio.Lock()
|
|
||||||
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
|
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
|
||||||
|
|
||||||
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
|
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
|
||||||
@ -136,39 +135,14 @@ async def async_setup_entry( # noqa: C901
|
|||||||
deprecated_address,
|
deprecated_address,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_stop_event_listener(event: Event) -> None:
|
manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager(
|
||||||
await asyncio.gather(
|
hass, entry, data, hosts
|
||||||
*[speaker.async_unsubscribe() for speaker in data.discovered.values()],
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
if events_asyncio.event_listener:
|
hass.async_create_task(manager.setup_platforms_and_discovery())
|
||||||
await events_asyncio.event_listener.async_stop()
|
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:
|
def _create_soco(ip_address: str, source: SoCoCreationSource) -> 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."""
|
"""Create a soco instance and return if successful."""
|
||||||
try:
|
try:
|
||||||
soco = pysonos.SoCo(ip_address)
|
soco = pysonos.SoCo(ip_address)
|
||||||
@ -182,104 +156,160 @@ async def async_setup_entry( # noqa: C901
|
|||||||
)
|
)
|
||||||
return None
|
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."""
|
"""Players from network configuration."""
|
||||||
for host in hosts:
|
for host in self.hosts:
|
||||||
ip_addr = socket.gethostbyname(host)
|
ip_addr = socket.gethostbyname(host)
|
||||||
known_uid = next(
|
known_uid = next(
|
||||||
(
|
(
|
||||||
uid
|
uid
|
||||||
for uid, speaker in data.discovered.items()
|
for uid, speaker in self.data.discovered.items()
|
||||||
if speaker.soco.ip_address == ip_addr
|
if speaker.soco.ip_address == ip_addr
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if known_uid:
|
if known_uid:
|
||||||
dispatcher_send(hass, f"{SONOS_SEEN}-{known_uid}")
|
dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}")
|
||||||
else:
|
else:
|
||||||
soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED)
|
soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED)
|
||||||
if soco and soco.is_visible:
|
if soco and soco.is_visible:
|
||||||
_discovered_player(soco)
|
self._discovered_player(soco)
|
||||||
|
|
||||||
data.hosts_heartbeat = hass.helpers.event.call_later(
|
self.data.hosts_heartbeat = self.hass.helpers.event.call_later(
|
||||||
DISCOVERY_INTERVAL.total_seconds(), _manual_hosts
|
DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_signal_update_groups(event):
|
def _async_signal_update_groups(self, _event):
|
||||||
async_dispatcher_send(hass, SONOS_GROUP_UPDATE)
|
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)
|
soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED)
|
||||||
if soco and soco.is_visible:
|
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."""
|
"""Only create one player at a time."""
|
||||||
async with discovery_lock:
|
async with self.discovery_lock:
|
||||||
if uid not in data.discovered:
|
if uid not in self.data.discovered:
|
||||||
await hass.async_add_executor_job(_discovered_ip, discovered_ip)
|
await self.hass.async_add_executor_job(
|
||||||
|
self._discovered_ip, discovered_ip
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if boot_seqnum and boot_seqnum > data.boot_counts[uid]:
|
if boot_seqnum and boot_seqnum > self.data.boot_counts[uid]:
|
||||||
data.boot_counts[uid] = boot_seqnum
|
self.data.boot_counts[uid] = boot_seqnum
|
||||||
if soco := await hass.async_add_executor_job(
|
if soco := await self.hass.async_add_executor_job(
|
||||||
_create_soco, discovered_ip, SoCoCreationSource.REBOOTED
|
_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:
|
else:
|
||||||
async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}")
|
async_dispatcher_send(self.hass, f"{SONOS_SEEN}-{uid}")
|
||||||
|
|
||||||
@callback
|
@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)
|
uid = info.get(ssdp.ATTR_UPNP_UDN)
|
||||||
if uid.startswith("uuid:"):
|
if uid.startswith("uuid:"):
|
||||||
uid = uid[5:]
|
uid = uid[5:]
|
||||||
if boot_seqnum := info.get("X-RINCON-BOOTSEQ"):
|
self.async_discovered_player(
|
||||||
boot_seqnum = int(boot_seqnum)
|
info, discovered_ip, uid, boot_seqnum, info.get("modelName")
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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(
|
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
|
for platform in PLATFORMS
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_START, _async_signal_update_groups
|
EVENT_HOMEASSISTANT_START, self._async_signal_update_groups
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener
|
EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Adding discovery job")
|
_LOGGER.debug("Adding discovery job")
|
||||||
if hosts:
|
if self.hosts:
|
||||||
entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat
|
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
|
return
|
||||||
|
|
||||||
entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
ssdp.async_register_callback(
|
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."""
|
"""Config flow for SONOS."""
|
||||||
|
import logging
|
||||||
|
|
||||||
import pysonos
|
import pysonos
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
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:
|
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||||
@ -13,4 +22,37 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
|
|||||||
return bool(result)
|
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"
|
DOMAIN = "sonos"
|
||||||
DATA_SONOS = "sonos_media_player"
|
DATA_SONOS = "sonos_media_player"
|
||||||
|
DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager"
|
||||||
PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN}
|
PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN}
|
||||||
|
|
||||||
SONOS_ARTIST = "artists"
|
SONOS_ARTIST = "artists"
|
||||||
@ -154,3 +155,5 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
|||||||
DISCOVERY_INTERVAL = datetime.timedelta(seconds=60)
|
DISCOVERY_INTERVAL = datetime.timedelta(seconds=60)
|
||||||
SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL
|
SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL
|
||||||
SUBSCRIPTION_TIMEOUT = 1200
|
SUBSCRIPTION_TIMEOUT = 1200
|
||||||
|
|
||||||
|
MDNS_SERVICE = "_sonos._tcp.local."
|
||||||
|
@ -9,6 +9,9 @@ from pysonos.exceptions import SoCoException, SoCoUPnPException
|
|||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
UID_PREFIX = "RINCON_"
|
||||||
|
UID_POSTFIX = "01400"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -36,3 +39,19 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
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",
|
"name": "Sonos",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||||
"requirements": ["pysonos==0.0.51"],
|
"requirements": ["pysonos==0.0.52"],
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"after_dependencies": ["plex"],
|
"after_dependencies": ["plex", "zeroconf"],
|
||||||
|
"zeroconf": ["_sonos._tcp.local."],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
"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.plugins.sharelink import ShareLinkPlugin
|
||||||
from pysonos.snapshot import Snapshot
|
from pysonos.snapshot import Snapshot
|
||||||
|
|
||||||
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
@ -37,6 +38,7 @@ from .const import (
|
|||||||
BATTERY_SCAN_INTERVAL,
|
BATTERY_SCAN_INTERVAL,
|
||||||
DATA_SONOS,
|
DATA_SONOS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
MDNS_SERVICE,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SCAN_INTERVAL,
|
SCAN_INTERVAL,
|
||||||
SEEN_EXPIRE_TIME,
|
SEEN_EXPIRE_TIME,
|
||||||
@ -56,7 +58,7 @@ from .const import (
|
|||||||
SUBSCRIPTION_TIMEOUT,
|
SUBSCRIPTION_TIMEOUT,
|
||||||
)
|
)
|
||||||
from .favorites import SonosFavorites
|
from .favorites import SonosFavorites
|
||||||
from .helpers import soco_error
|
from .helpers import soco_error, uid_to_short_hostname
|
||||||
|
|
||||||
EVENT_CHARGING = {
|
EVENT_CHARGING = {
|
||||||
"CHARGING": True,
|
"CHARGING": True,
|
||||||
@ -498,12 +500,27 @@ class SonosSpeaker:
|
|||||||
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Make this player unavailable when it was not seen recently."""
|
"""Make this player unavailable when it was not seen recently."""
|
||||||
self._share_link_plugin = None
|
|
||||||
|
|
||||||
if self._seen_timer:
|
if self._seen_timer:
|
||||||
self._seen_timer()
|
self._seen_timer()
|
||||||
self._seen_timer = None
|
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:
|
if self._poll_timer:
|
||||||
self._poll_timer()
|
self._poll_timer()
|
||||||
self._poll_timer = None
|
self._poll_timer = None
|
||||||
@ -511,7 +528,7 @@ class SonosSpeaker:
|
|||||||
await self.async_unsubscribe()
|
await self.async_unsubscribe()
|
||||||
|
|
||||||
if not will_reconnect:
|
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()
|
self.async_write_entity_states()
|
||||||
|
|
||||||
async def async_rebooted(self, soco: SoCo) -> None:
|
async def async_rebooted(self, soco: SoCo) -> None:
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
|
"not_sonos_device": "Discovered device is not a Sonos device",
|
||||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"no_devices_found": "No devices found on the network",
|
"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."
|
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
@ -29,6 +29,8 @@ from .flow import FlowDispatcher, SSDPFlow
|
|||||||
DOMAIN = "ssdp"
|
DOMAIN = "ssdp"
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
|
IPV4_BROADCAST = IPv4Address("255.255.255.255")
|
||||||
|
|
||||||
# Attributes for accessing info from SSDP response
|
# Attributes for accessing info from SSDP response
|
||||||
ATTR_SSDP_LOCATION = "ssdp_location"
|
ATTR_SSDP_LOCATION = "ssdp_location"
|
||||||
ATTR_SSDP_ST = "ssdp_st"
|
ATTR_SSDP_ST = "ssdp_st"
|
||||||
@ -236,7 +238,20 @@ class Scanner:
|
|||||||
async_callback=self._async_process_entry, source_ip=source_ip
|
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_STOP, self.async_stop)
|
||||||
self.hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start
|
EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start
|
||||||
|
@ -158,10 +158,10 @@ class SurePetcareAPI:
|
|||||||
# https://github.com/PyCQA/pylint/issues/2062
|
# https://github.com/PyCQA/pylint/issues/2062
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
if state == LockState.UNLOCKED.name.lower():
|
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():
|
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():
|
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():
|
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",
|
"name": "Version",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/version",
|
"documentation": "https://www.home-assistant.io/integrations/version",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pyhaversion==21.5.0"
|
"pyhaversion==21.7.0"
|
||||||
],
|
],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@fabaff",
|
"@fabaff",
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"zha-quirks==0.0.59",
|
"zha-quirks==0.0.59",
|
||||||
"zigpy-cc==0.5.2",
|
"zigpy-cc==0.5.2",
|
||||||
"zigpy-deconz==0.12.0",
|
"zigpy-deconz==0.12.0",
|
||||||
"zigpy==0.35.1",
|
"zigpy==0.35.2",
|
||||||
"zigpy-xbee==0.13.0",
|
"zigpy-xbee==0.13.0",
|
||||||
"zigpy-zigate==0.7.3",
|
"zigpy-zigate==0.7.3",
|
||||||
"zigpy-znp==0.5.1"
|
"zigpy-znp==0.5.1"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Z-Wave JS",
|
"name": "Z-Wave JS",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
"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"],
|
"codeowners": ["@home-assistant/z-wave"],
|
||||||
"dependencies": ["http", "websocket_api"],
|
"dependencies": ["http", "websocket_api"],
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
|
@ -5,7 +5,7 @@ from typing import Final
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 7
|
MINOR_VERSION: Final = 7
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__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, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -186,6 +186,11 @@ ZEROCONF = {
|
|||||||
"name": "brother*"
|
"name": "brother*"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"_sonos._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "sonos"
|
||||||
|
}
|
||||||
|
],
|
||||||
"_spotify-connect._tcp.local.": [
|
"_spotify-connect._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "spotify"
|
"domain": "spotify"
|
||||||
|
@ -160,7 +160,7 @@ aioeafm==0.1.2
|
|||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==5.0.0
|
aioesphomeapi==5.0.1
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==0.4.1
|
aioflo==0.4.1
|
||||||
@ -175,7 +175,7 @@ aioguardian==1.0.4
|
|||||||
aioharmony==0.2.7
|
aioharmony==0.2.7
|
||||||
|
|
||||||
# homeassistant.components.homekit_controller
|
# homeassistant.components.homekit_controller
|
||||||
aiohomekit==0.4.2
|
aiohomekit==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.emulated_hue
|
# homeassistant.components.emulated_hue
|
||||||
# homeassistant.components.http
|
# homeassistant.components.http
|
||||||
@ -290,7 +290,7 @@ aprslib==0.6.46
|
|||||||
aqualogic==2.6
|
aqualogic==2.6
|
||||||
|
|
||||||
# homeassistant.components.arcam_fmj
|
# homeassistant.components.arcam_fmj
|
||||||
arcam-fmj==0.5.3
|
arcam-fmj==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.arris_tg2492lg
|
# homeassistant.components.arris_tg2492lg
|
||||||
arris-tg2492lg==1.1.0
|
arris-tg2492lg==1.1.0
|
||||||
@ -1020,7 +1020,7 @@ nettigo-air-monitor==1.0.0
|
|||||||
neurio==0.3.1
|
neurio==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.nexia
|
# homeassistant.components.nexia
|
||||||
nexia==0.9.7
|
nexia==0.9.9
|
||||||
|
|
||||||
# homeassistant.components.nextcloud
|
# homeassistant.components.nextcloud
|
||||||
nextcloudmonitor==1.1.0
|
nextcloudmonitor==1.1.0
|
||||||
@ -1375,7 +1375,7 @@ pydaikin==2.4.4
|
|||||||
pydanfossair==0.1.0
|
pydanfossair==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==80
|
pydeconz==81
|
||||||
|
|
||||||
# homeassistant.components.delijn
|
# homeassistant.components.delijn
|
||||||
pydelijn==0.6.1
|
pydelijn==0.6.1
|
||||||
@ -1423,7 +1423,7 @@ pyezviz==0.1.8.9
|
|||||||
pyfido==2.1.1
|
pyfido==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.fireservicerota
|
# homeassistant.components.fireservicerota
|
||||||
pyfireservicerota==0.0.40
|
pyfireservicerota==0.0.42
|
||||||
|
|
||||||
# homeassistant.components.flexit
|
# homeassistant.components.flexit
|
||||||
pyflexit==0.3
|
pyflexit==0.3
|
||||||
@ -1466,7 +1466,7 @@ pygtfs==0.1.6
|
|||||||
pygti==0.9.2
|
pygti==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.version
|
# homeassistant.components.version
|
||||||
pyhaversion==21.5.0
|
pyhaversion==21.7.0
|
||||||
|
|
||||||
# homeassistant.components.heos
|
# homeassistant.components.heos
|
||||||
pyheos==0.7.2
|
pyheos==0.7.2
|
||||||
@ -1490,7 +1490,7 @@ pyialarm==1.9.0
|
|||||||
pyicloud==0.10.2
|
pyicloud==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.insteon
|
# homeassistant.components.insteon
|
||||||
pyinsteon==1.0.9
|
pyinsteon==1.0.11
|
||||||
|
|
||||||
# homeassistant.components.intesishome
|
# homeassistant.components.intesishome
|
||||||
pyintesishome==1.7.6
|
pyintesishome==1.7.6
|
||||||
@ -1571,7 +1571,7 @@ pymailgunner==1.4
|
|||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
# homeassistant.components.mazda
|
# homeassistant.components.mazda
|
||||||
pymazda==0.1.6
|
pymazda==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.mediaroom
|
# homeassistant.components.mediaroom
|
||||||
pymediaroom==0.6.4.1
|
pymediaroom==0.6.4.1
|
||||||
@ -1773,7 +1773,7 @@ pysnmp==4.4.12
|
|||||||
pysoma==0.0.10
|
pysoma==0.0.10
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
pysonos==0.0.51
|
pysonos==0.0.52
|
||||||
|
|
||||||
# homeassistant.components.spc
|
# homeassistant.components.spc
|
||||||
pyspcwebgw==0.4.0
|
pyspcwebgw==0.4.0
|
||||||
@ -2451,10 +2451,10 @@ zigpy-zigate==0.7.3
|
|||||||
zigpy-znp==0.5.1
|
zigpy-znp==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.35.1
|
zigpy==0.35.2
|
||||||
|
|
||||||
# homeassistant.components.zoneminder
|
# homeassistant.components.zoneminder
|
||||||
zm-py==0.5.2
|
zm-py==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# 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
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==5.0.0
|
aioesphomeapi==5.0.1
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==0.4.1
|
aioflo==0.4.1
|
||||||
@ -112,7 +112,7 @@ aioguardian==1.0.4
|
|||||||
aioharmony==0.2.7
|
aioharmony==0.2.7
|
||||||
|
|
||||||
# homeassistant.components.homekit_controller
|
# homeassistant.components.homekit_controller
|
||||||
aiohomekit==0.4.2
|
aiohomekit==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.emulated_hue
|
# homeassistant.components.emulated_hue
|
||||||
# homeassistant.components.http
|
# homeassistant.components.http
|
||||||
@ -191,7 +191,7 @@ apprise==0.9.3
|
|||||||
aprslib==0.6.46
|
aprslib==0.6.46
|
||||||
|
|
||||||
# homeassistant.components.arcam_fmj
|
# homeassistant.components.arcam_fmj
|
||||||
arcam-fmj==0.5.3
|
arcam-fmj==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.dlna_dmr
|
# homeassistant.components.dlna_dmr
|
||||||
# homeassistant.components.ssdp
|
# homeassistant.components.ssdp
|
||||||
@ -574,7 +574,7 @@ netdisco==2.9.0
|
|||||||
nettigo-air-monitor==1.0.0
|
nettigo-air-monitor==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.nexia
|
# homeassistant.components.nexia
|
||||||
nexia==0.9.7
|
nexia==0.9.9
|
||||||
|
|
||||||
# homeassistant.components.notify_events
|
# homeassistant.components.notify_events
|
||||||
notify-events==1.0.4
|
notify-events==1.0.4
|
||||||
@ -770,7 +770,7 @@ pycoolmasternet-async==0.1.2
|
|||||||
pydaikin==2.4.4
|
pydaikin==2.4.4
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==80
|
pydeconz==81
|
||||||
|
|
||||||
# homeassistant.components.dexcom
|
# homeassistant.components.dexcom
|
||||||
pydexcom==0.2.0
|
pydexcom==0.2.0
|
||||||
@ -791,7 +791,7 @@ pyezviz==0.1.8.9
|
|||||||
pyfido==2.1.1
|
pyfido==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.fireservicerota
|
# homeassistant.components.fireservicerota
|
||||||
pyfireservicerota==0.0.40
|
pyfireservicerota==0.0.42
|
||||||
|
|
||||||
# homeassistant.components.flume
|
# homeassistant.components.flume
|
||||||
pyflume==0.5.5
|
pyflume==0.5.5
|
||||||
@ -819,7 +819,7 @@ pygatt[GATTTOOL]==4.0.5
|
|||||||
pygti==0.9.2
|
pygti==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.version
|
# homeassistant.components.version
|
||||||
pyhaversion==21.5.0
|
pyhaversion==21.7.0
|
||||||
|
|
||||||
# homeassistant.components.heos
|
# homeassistant.components.heos
|
||||||
pyheos==0.7.2
|
pyheos==0.7.2
|
||||||
@ -837,7 +837,7 @@ pyialarm==1.9.0
|
|||||||
pyicloud==0.10.2
|
pyicloud==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.insteon
|
# homeassistant.components.insteon
|
||||||
pyinsteon==1.0.9
|
pyinsteon==1.0.11
|
||||||
|
|
||||||
# homeassistant.components.ipma
|
# homeassistant.components.ipma
|
||||||
pyipma==2.0.5
|
pyipma==2.0.5
|
||||||
@ -888,7 +888,7 @@ pymailgunner==1.4
|
|||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
# homeassistant.components.mazda
|
# homeassistant.components.mazda
|
||||||
pymazda==0.1.6
|
pymazda==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.melcloud
|
# homeassistant.components.melcloud
|
||||||
pymelcloud==2.5.3
|
pymelcloud==2.5.3
|
||||||
@ -1006,7 +1006,7 @@ pysmartthings==0.7.6
|
|||||||
pysoma==0.0.10
|
pysoma==0.0.10
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
pysonos==0.0.51
|
pysonos==0.0.52
|
||||||
|
|
||||||
# homeassistant.components.spc
|
# homeassistant.components.spc
|
||||||
pyspcwebgw==0.4.0
|
pyspcwebgw==0.4.0
|
||||||
@ -1345,7 +1345,7 @@ zigpy-zigate==0.7.3
|
|||||||
zigpy-znp==0.5.1
|
zigpy-znp==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.35.1
|
zigpy==0.35.2
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# 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, PRESSURE_SURFACE_LEVEL, "997.9688")
|
||||||
check_sensor_state(hass, GHI, "0.0")
|
check_sensor_state(hass, GHI, "0.0")
|
||||||
check_sensor_state(hass, CLOUD_BASE, "1.1909")
|
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, CLOUD_CEILING, "1.1909")
|
||||||
check_sensor_state(hass, WIND_GUST, "5.6506")
|
check_sensor_state(hass, WIND_GUST, "5.6506")
|
||||||
check_sensor_state(hass, PRECIPITATION_TYPE, "rain")
|
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_VISIBILITY] == 9.9940
|
||||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
|
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_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_WIND_GUST] == 24.0758
|
||||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
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_VISIBILITY] == 13.1162
|
||||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14
|
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_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_WIND_GUST] == 20.3421
|
||||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
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(
|
@pytest.mark.parametrize(
|
||||||
"input,expected",
|
"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
|
( # Turn on light with short color loop
|
||||||
{
|
{
|
||||||
"light_on": False,
|
"light_on": False,
|
||||||
@ -811,9 +839,8 @@ async def test_groups(hass, aioclient_mock, input, expected):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hue": 45510,
|
|
||||||
"on": True,
|
"on": True,
|
||||||
"sat": 127,
|
"xy": (0.235, 0.164),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
( # Turn on group with short color loop
|
( # Turn on group with short color loop
|
||||||
@ -827,9 +854,8 @@ async def test_groups(hass, aioclient_mock, input, expected):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hue": 45510,
|
|
||||||
"on": True,
|
"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
|
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):
|
async def test_humidity_sensor_read_state(hass, utcnow):
|
||||||
"""Test reading the state of a HomeKit humidity sensor accessory."""
|
"""Test reading the state of a HomeKit humidity sensor accessory."""
|
||||||
helper = await setup_test_component(
|
helper = await setup_test_component(
|
||||||
|
@ -97,7 +97,7 @@ async def test_update_auth_failure(hass: HomeAssistant):
|
|||||||
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||||
side_effect=MazdaAuthenticationException("Login failed"),
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
flows = hass.config_entries.flow.async_progress()
|
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",
|
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||||
side_effect=Exception("Unknown exception"),
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
|
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()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert async_start_mock.call_count == 1
|
assert async_start_mock.call_count == 2
|
||||||
assert async_search_mock.call_count == 1
|
assert async_search_mock.call_count == 2
|
||||||
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert async_start_mock.call_count == 1
|
assert async_start_mock.call_count == 2
|
||||||
assert async_search_mock.call_count == 1
|
assert async_search_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog):
|
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()
|
await hass.async_block_till_done()
|
||||||
assert hass.state == CoreState.running
|
assert hass.state == CoreState.running
|
||||||
|
|
||||||
assert len(integration_callbacks) == 3
|
assert len(integration_callbacks) == 5
|
||||||
assert len(integration_callbacks_from_cache) == 3
|
assert len(integration_callbacks_from_cache) == 5
|
||||||
assert len(integration_match_all_callbacks) == 3
|
assert len(integration_match_all_callbacks) == 5
|
||||||
assert len(integration_match_all_not_present_callbacks) == 0
|
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 len(not_matching_integration_callbacks) == 0
|
||||||
assert integration_callbacks[0] == {
|
assert integration_callbacks[0] == {
|
||||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
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 hass.state == CoreState.running
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
len(integration_callbacks) == 2
|
len(integration_callbacks) == 4
|
||||||
) # unsolicited callbacks without st are not cached
|
) # unsolicited callbacks without st are not cached
|
||||||
assert integration_callbacks[0] == {
|
assert integration_callbacks[0] == {
|
||||||
"UDN": "uuid:RINCON_1111BB963FD801400",
|
"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))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(integration_callbacks) == 2
|
assert len(integration_callbacks) == 4
|
||||||
assert integration_callbacks[0] == {
|
assert integration_callbacks[0] == {
|
||||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
||||||
ssdp.ATTR_SSDP_EXT: "",
|
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)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert {create_args[0][1]["source_ip"], create_args[1][1]["source_ip"]} == {
|
argset = set()
|
||||||
IPv4Address("192.168.1.5"),
|
for argmap in create_args:
|
||||||
IPv6Address("2001:db8::"),
|
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