Merge pull request #53076 from home-assistant/rc

2021.7.3
This commit is contained in:
Franck Nijhof 2021-07-16 10:28:42 +02:00 committed by GitHub
commit bcab1414f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 343 additions and 96 deletions

View File

@ -633,6 +633,9 @@ omit =
homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/camera.py
homeassistant/components/mochad/* homeassistant/components/mochad/*
homeassistant/components/modbus/base_platform.py
homeassistant/components/modbus/binary_sensor.py
homeassistant/components/modbus/cover.py
homeassistant/components/modbus/climate.py homeassistant/components/modbus/climate.py
homeassistant/components/modbus/modbus.py homeassistant/components/modbus/modbus.py
homeassistant/components/modem_callerid/sensor.py homeassistant/components/modem_callerid/sensor.py

View File

@ -3,7 +3,7 @@
"name": "Apple TV", "name": "Apple TV",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apple_tv", "documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.7.7"], "requirements": ["pyatv==0.8.1"],
"zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."],
"after_dependencies": ["discovery"], "after_dependencies": ["discovery"],
"codeowners": ["@postlund"], "codeowners": ["@postlund"],

View File

@ -1,4 +1,5 @@
"""Support for the CO2signal platform.""" """Support for the CO2signal platform."""
from datetime import timedelta
import logging import logging
import CO2Signal import CO2Signal
@ -17,6 +18,7 @@ import homeassistant.helpers.config_validation as cv
CONF_COUNTRY_CODE = "country_code" CONF_COUNTRY_CODE = "country_code"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=3)
ATTRIBUTION = "Data provided by CO2signal" ATTRIBUTION = "Data provided by CO2signal"

View File

@ -248,10 +248,10 @@ class DeviceTrackerWatcher(WatcherBase):
return return
ip_address = attributes.get(ATTR_IP) ip_address = attributes.get(ATTR_IP)
hostname = attributes.get(ATTR_HOST_NAME) hostname = attributes.get(ATTR_HOST_NAME, "")
mac_address = attributes.get(ATTR_MAC) mac_address = attributes.get(ATTR_MAC)
if ip_address is None or hostname is None or mac_address is None: if ip_address is None or mac_address is None:
return return
self.process_client(ip_address, hostname, _format_mac(mac_address)) self.process_client(ip_address, hostname, _format_mac(mac_address))
@ -328,10 +328,10 @@ class DHCPWatcher(WatcherBase):
return return
ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src
hostname = _decode_dhcp_option(options, HOSTNAME) hostname = _decode_dhcp_option(options, HOSTNAME) or ""
mac_address = _format_mac(packet[Ether].src) mac_address = _format_mac(packet[Ether].src)
if ip_address is None or hostname is None or mac_address is None: if ip_address is None or mac_address is None:
return return
self.process_client(ip_address, hostname, mac_address) self.process_client(ip_address, hostname, mac_address)

View File

@ -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.42"], "requirements": ["pyfireservicerota==0.0.43"],
"codeowners": ["@cyberjunky"], "codeowners": ["@cyberjunky"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
name="home_plus_control_module", name="home_plus_control_module",
update_method=async_update_data, update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers. # Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=60), update_interval=timedelta(seconds=300),
) )
hass_entry_data[DATA_COORDINATOR] = coordinator hass_entry_data[DATA_COORDINATOR] = coordinator

View File

@ -3,7 +3,7 @@
"name": "Insteon", "name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon", "documentation": "https://www.home-assistant.io/integrations/insteon",
"requirements": [ "requirements": [
"pyinsteon==1.0.11" "pyinsteon==1.0.12"
], ],
"codeowners": [ "codeowners": [
"@teharris1" "@teharris1"

View File

@ -100,10 +100,8 @@ class KNXExposeSensor:
def _init_expose_state(self) -> None: def _init_expose_state(self) -> None:
"""Initialize state of the exposure.""" """Initialize state of the exposure."""
init_state = self.hass.states.get(self.entity_id) init_state = self.hass.states.get(self.entity_id)
init_value = self._get_expose_value(init_state) state_value = self._get_expose_value(init_state)
self.device.sensor_value.value = ( self.device.sensor_value.value = state_value
init_value if init_value is not None else self.expose_default
)
@callback @callback
def shutdown(self) -> None: def shutdown(self) -> None:
@ -116,11 +114,12 @@ class KNXExposeSensor:
def _get_expose_value(self, state: State | None) -> StateType: def _get_expose_value(self, state: State | None) -> StateType:
"""Extract value from state.""" """Extract value from state."""
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
return None value = self.expose_default
else:
value = ( value = (
state.state state.state
if self.expose_attribute is None if self.expose_attribute is None
else state.attributes.get(self.expose_attribute) else state.attributes.get(self.expose_attribute, self.expose_default)
) )
if self.type == "binary": if self.type == "binary":
if value in (1, STATE_ON, "True"): if value in (1, STATE_ON, "True"):
@ -150,9 +149,7 @@ class KNXExposeSensor:
async def _async_set_knx_value(self, value: StateType) -> None: async def _async_set_knx_value(self, value: StateType) -> None:
"""Set new value on xknx ExposeSensor.""" """Set new value on xknx ExposeSensor."""
if value is None: if value is None:
if self.expose_default is None:
return return
value = self.expose_default
await self.device.set(value) await self.device.set(value)

View File

@ -3,7 +3,7 @@
"name": "LCN", "name": "LCN",
"config_flow": false, "config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/lcn", "documentation": "https://www.home-assistant.io/integrations/lcn",
"requirements": ["pypck==0.7.9"], "requirements": ["pypck==0.7.10"],
"codeowners": ["@alengwenus"], "codeowners": ["@alengwenus"],
"iot_class": "local_push" "iot_class": "local_push"
} }

View File

@ -265,6 +265,17 @@ turn_on:
min: -100 min: -100
max: 100 max: 100
unit_of_measurement: "%" unit_of_measurement: "%"
white:
name: White
description:
Set the light to white mode and change its brightness, where 0 turns
the light off, 1 is the minimum brightness and 255 is the maximum
brightness supported by the light.
advanced: true
selector:
number:
min: 0
max: 255
profile: profile:
name: Profile name: Profile
description: Name of a light profile to use. description: Name of a light profile to use.

View File

@ -51,6 +51,7 @@ class BasePlatform(Entity):
self._value = None self._value = None
self._available = True self._available = True
self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
self._call_active = False
@abstractmethod @abstractmethod
async def async_update(self, now=None): async def async_update(self, now=None):
@ -160,9 +161,14 @@ class BaseSwitch(BasePlatform, RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
return return
# do not allow multiple active calls to the same platform
if self._call_active:
return
self._call_active = True
result = await self._hub.async_pymodbus_call( result = await self._hub.async_pymodbus_call(
self._slave, self._verify_address, 1, self._verify_type self._slave, self._verify_address, 1, self._verify_type
) )
self._call_active = False
if result is None: if result is None:
self._available = False self._available = False
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -54,9 +54,15 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
async def async_update(self, now=None): async def async_update(self, now=None):
"""Update the state of the sensor.""" """Update the state of the sensor."""
# do not allow multiple active calls to the same platform
if self._call_active:
return
self._call_active = True
result = await self._hub.async_pymodbus_call( result = await self._hub.async_pymodbus_call(
self._slave, self._address, 1, self._input_type self._slave, self._address, 1, self._input_type
) )
self._call_active = False
if result is None: if result is None:
self._available = False self._available = False
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -185,13 +185,18 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
"""Update Target & Current Temperature.""" """Update Target & Current Temperature."""
# remark "now" is a dummy parameter to avoid problems with # remark "now" is a dummy parameter to avoid problems with
# async_track_time_interval # async_track_time_interval
# do not allow multiple active calls to the same platform
if self._call_active:
return
self._call_active = True
self._target_temperature = await self._async_read_register( self._target_temperature = await self._async_read_register(
CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
) )
self._current_temperature = await self._async_read_register( self._current_temperature = await self._async_read_register(
self._input_type, self._address self._input_type, self._address
) )
self._call_active = False
self.async_write_ha_state() self.async_write_ha_state()
async def _async_read_register(self, register_type, register) -> float | None: async def _async_read_register(self, register_type, register) -> float | None:

View File

@ -149,9 +149,14 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
"""Update the state of the cover.""" """Update the state of the cover."""
# remark "now" is a dummy parameter to avoid problems with # remark "now" is a dummy parameter to avoid problems with
# async_track_time_interval # async_track_time_interval
# do not allow multiple active calls to the same platform
if self._call_active:
return
self._call_active = True
result = await self._hub.async_pymodbus_call( result = await self._hub.async_pymodbus_call(
self._slave, self._address, 1, self._input_type self._slave, self._address, 1, self._input_type
) )
self._call_active = False
if result is None: if result is None:
self._available = False self._available = False
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -1,5 +1,6 @@
"""Support for Modbus.""" """Support for Modbus."""
import asyncio import asyncio
from copy import deepcopy
import logging import logging
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
@ -196,7 +197,7 @@ class ModbusHub:
self._config_name = client_config[CONF_NAME] self._config_name = client_config[CONF_NAME]
self._config_type = client_config[CONF_TYPE] self._config_type = client_config[CONF_TYPE]
self._config_delay = client_config[CONF_DELAY] self._config_delay = client_config[CONF_DELAY]
self._pb_call = PYMODBUS_CALL.copy() self._pb_call = deepcopy(PYMODBUS_CALL)
self._pb_class = { self._pb_class = {
CONF_SERIAL: ModbusSerialClient, CONF_SERIAL: ModbusSerialClient,
CONF_TCP: ModbusTcpClient, CONF_TCP: ModbusTcpClient,

View File

@ -2,6 +2,7 @@
import logging import logging
from plexapi.exceptions import NotFound from plexapi.exceptions import NotFound
import requests.exceptions
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
@ -171,6 +172,13 @@ class PlexLibrarySectionSensor(SensorEntity):
self._available = True self._available = True
except NotFound: except NotFound:
self._available = False self._available = False
except requests.exceptions.RequestException as err:
_LOGGER.error(
"Could not update library sensor for '%s': %s",
self.library_section.title,
err,
)
self._available = False
self.async_write_ha_state() self.async_write_ha_state()
def _update_state_and_attrs(self): def _update_state_and_attrs(self):

View File

@ -2,7 +2,7 @@
"domain": "rainbird", "domain": "rainbird",
"name": "Rain Bird", "name": "Rain Bird",
"documentation": "https://www.home-assistant.io/integrations/rainbird", "documentation": "https://www.home-assistant.io/integrations/rainbird",
"requirements": ["pyrainbird==0.4.2"], "requirements": ["pyrainbird==0.4.3"],
"codeowners": ["@konikvranik"], "codeowners": ["@konikvranik"],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -18,7 +18,11 @@
"dhcp": [ "dhcp": [
{ {
"hostname": "tizen*" "hostname": "tizen*"
} },
{"macaddress": "8CC8CD*"},
{"macaddress": "606BBD*"},
{"macaddress": "F47B5E*"},
{"macaddress": "4844F7*"}
], ],
"codeowners": [ "codeowners": [
"@escoand", "@escoand",

View File

@ -6,6 +6,8 @@ from typing import Any
from pysiaalarm import SIAEvent from pysiaalarm import SIAEvent
from homeassistant.util.dt import utcnow
from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE
PING_INTERVAL_MARGIN = 30 PING_INTERVAL_MARGIN = 30
@ -23,7 +25,9 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]:
ATTR_CODE: event.code, ATTR_CODE: event.code,
ATTR_MESSAGE: event.message, ATTR_MESSAGE: event.message,
ATTR_ID: event.id, ATTR_ID: event.id,
ATTR_TIMESTAMP: event.timestamp.isoformat(), ATTR_TIMESTAMP: event.timestamp.isoformat()
if event.timestamp
else utcnow().isoformat(),
} }
@ -42,7 +46,9 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]:
"code": event.code, "code": event.code,
"message": event.message, "message": event.message,
"x_data": event.x_data, "x_data": event.x_data,
"timestamp": event.timestamp.isoformat(), "timestamp": event.timestamp.isoformat()
if event.timestamp
else utcnow().isoformat(),
"event_qualifier": event.event_qualifier, "event_qualifier": event.event_qualifier,
"event_type": event.event_type, "event_type": event.event_type,
"partition": event.partition, "partition": event.partition,

View File

@ -3,7 +3,7 @@
"name": "SMA Solar", "name": "SMA Solar",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sma", "documentation": "https://www.home-assistant.io/integrations/sma",
"requirements": ["pysma==0.6.2"], "requirements": ["pysma==0.6.4"],
"codeowners": ["@kellerza", "@rklomp"], "codeowners": ["@kellerza", "@rklomp"],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -266,6 +266,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
"manufacturer": "Spotify AB", "manufacturer": "Spotify AB",
"model": model, "model": model,
"name": self._name, "name": self._name,
"entry_type": "service",
} }
@property @property

View File

@ -183,12 +183,13 @@ class ZHADevice(LogMixin):
return self._zigpy_device.model return self._zigpy_device.model
@property @property
def manufacturer_code(self): def manufacturer_code(self) -> int | None:
"""Return the manufacturer code for the device.""" """Return the manufacturer code for the device."""
if self._zigpy_device.node_desc.is_valid: if self._zigpy_device.node_desc is None:
return self._zigpy_device.node_desc.manufacturer_code
return None return None
return self._zigpy_device.node_desc.manufacturer_code
@property @property
def nwk(self): def nwk(self):
"""Return nwk for device.""" """Return nwk for device."""
@ -210,17 +211,20 @@ class ZHADevice(LogMixin):
return self._zigpy_device.last_seen return self._zigpy_device.last_seen
@property @property
def is_mains_powered(self): def is_mains_powered(self) -> bool | None:
"""Return true if device is mains powered.""" """Return true if device is mains powered."""
if self._zigpy_device.node_desc is None:
return None
return self._zigpy_device.node_desc.is_mains_powered return self._zigpy_device.node_desc.is_mains_powered
@property @property
def device_type(self): def device_type(self) -> str:
"""Return the logical device type for the device.""" """Return the logical device type for the device."""
node_descriptor = self._zigpy_device.node_desc if self._zigpy_device.node_desc is None:
return ( return UNKNOWN
node_descriptor.logical_type.name if node_descriptor.is_valid else UNKNOWN
) return self._zigpy_device.node_desc.logical_type.name
@property @property
def power_source(self): def power_source(self):
@ -230,18 +234,27 @@ class ZHADevice(LogMixin):
) )
@property @property
def is_router(self): def is_router(self) -> bool | None:
"""Return true if this is a routing capable device.""" """Return true if this is a routing capable device."""
if self._zigpy_device.node_desc is None:
return None
return self._zigpy_device.node_desc.is_router return self._zigpy_device.node_desc.is_router
@property @property
def is_coordinator(self): def is_coordinator(self) -> bool | None:
"""Return true if this device represents the coordinator.""" """Return true if this device represents the coordinator."""
if self._zigpy_device.node_desc is None:
return None
return self._zigpy_device.node_desc.is_coordinator return self._zigpy_device.node_desc.is_coordinator
@property @property
def is_end_device(self): def is_end_device(self) -> bool | None:
"""Return true if this device is an end device.""" """Return true if this device is an end device."""
if self._zigpy_device.node_desc is None:
return None
return self._zigpy_device.node_desc.is_end_device return self._zigpy_device.node_desc.is_end_device
@property @property

View File

@ -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 = "2" PATCH_VERSION: Final = "3"
__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)

View File

@ -175,6 +175,22 @@ DHCP = [
"domain": "samsungtv", "domain": "samsungtv",
"hostname": "tizen*" "hostname": "tizen*"
}, },
{
"domain": "samsungtv",
"macaddress": "8CC8CD*"
},
{
"domain": "samsungtv",
"macaddress": "606BBD*"
},
{
"domain": "samsungtv",
"macaddress": "F47B5E*"
},
{
"domain": "samsungtv",
"macaddress": "4844F7*"
},
{ {
"domain": "screenlogic", "domain": "screenlogic",
"hostname": "pentair: *", "hostname": "pentair: *",

View File

@ -1318,7 +1318,7 @@ pyatmo==5.2.0
pyatome==0.1.1 pyatome==0.1.1
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.7.7 pyatv==0.8.1
# homeassistant.components.bbox # homeassistant.components.bbox
pybbox==0.0.5-alpha pybbox==0.0.5-alpha
@ -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.42 pyfireservicerota==0.0.43
# homeassistant.components.flexit # homeassistant.components.flexit
pyflexit==0.3 pyflexit==0.3
@ -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.11 pyinsteon==1.0.12
# homeassistant.components.intesishome # homeassistant.components.intesishome
pyintesishome==1.7.6 pyintesishome==1.7.6
@ -1669,7 +1669,7 @@ pyownet==0.10.0.post1
pypca==0.0.7 pypca==0.0.7
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.7.9 pypck==0.7.10
# homeassistant.components.pjlink # homeassistant.components.pjlink
pypjlink2==1.2.1 pypjlink2==1.2.1
@ -1696,7 +1696,7 @@ pyqwikswitch==0.93
pyrail==0.0.3 pyrail==0.0.3
# homeassistant.components.rainbird # homeassistant.components.rainbird
pyrainbird==0.4.2 pyrainbird==0.4.3
# homeassistant.components.recswitch # homeassistant.components.recswitch
pyrecswitch==1.0.2 pyrecswitch==1.0.2
@ -1749,7 +1749,7 @@ pysignalclirestapi==0.3.4
pyskyqhub==0.1.3 pyskyqhub==0.1.3
# homeassistant.components.sma # homeassistant.components.sma
pysma==0.6.2 pysma==0.6.4
# homeassistant.components.smappee # homeassistant.components.smappee
pysmappee==0.2.25 pysmappee==0.2.25

View File

@ -743,7 +743,7 @@ pyatag==0.3.5.3
pyatmo==5.2.0 pyatmo==5.2.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.7.7 pyatv==0.8.1
# homeassistant.components.blackbird # homeassistant.components.blackbird
pyblackbird==0.5 pyblackbird==0.5
@ -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.42 pyfireservicerota==0.0.43
# homeassistant.components.flume # homeassistant.components.flume
pyflume==0.5.5 pyflume==0.5.5
@ -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.11 pyinsteon==1.0.12
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==2.0.5 pyipma==2.0.5
@ -950,7 +950,7 @@ pyowm==3.2.0
pyownet==0.10.0.post1 pyownet==0.10.0.post1
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.7.9 pypck==0.7.10
# homeassistant.components.plaato # homeassistant.components.plaato
pyplaato==0.0.15 pyplaato==0.0.15
@ -991,7 +991,7 @@ pysiaalarm==3.0.0
pysignalclirestapi==0.3.4 pysignalclirestapi==0.3.4
# homeassistant.components.sma # homeassistant.components.sma
pysma==0.6.2 pysma==0.6.4
# homeassistant.components.smappee # homeassistant.components.smappee
pysmappee==0.2.25 pysmappee==0.2.25

View File

@ -2,7 +2,8 @@
from unittest.mock import patch from unittest.mock import patch
from pyatv import conf, net from pyatv import conf
from pyatv.support.http import create_session
import pytest import pytest
from .common import MockPairingHandler, create_conf from .common import MockPairingHandler, create_conf
@ -39,7 +40,7 @@ def pairing():
async def _pair(config, protocol, loop, session=None, **kwargs): async def _pair(config, protocol, loop, session=None, **kwargs):
handler = MockPairingHandler( handler = MockPairingHandler(
await net.create_session(session), config.get_service(protocol) await create_session(session), config.get_service(protocol)
) )
handler.always_fail = mock_pair.always_fail handler.always_fail = mock_pair.always_fail
return handler return handler
@ -121,11 +122,7 @@ def dmap_device_with_credentials(mock_scan):
@pytest.fixture @pytest.fixture
def airplay_device(mock_scan): def device_with_no_services(mock_scan):
"""Mock pyatv.scan.""" """Mock pyatv.scan."""
mock_scan.result.append( mock_scan.result.append(create_conf("127.0.0.1", "Invalid Device"))
create_conf(
"127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777)
)
)
yield mock_scan yield mock_scan

View File

@ -236,15 +236,15 @@ async def test_user_adds_existing_device(hass, mrp_device):
assert result2["errors"] == {"base": "already_configured"} assert result2["errors"] == {"base": "already_configured"}
async def test_user_adds_unusable_device(hass, airplay_device): async def test_user_adds_unusable_device(hass, device_with_no_services):
"""Test that it is not possible to add pure AirPlay device.""" """Test that it is not possible to add device with no services."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"device_input": "AirPlay Device"}, {"device_input": "Invalid Device"},
) )
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "no_usable_service"} assert result2["errors"] == {"base": "no_usable_service"}

View File

@ -81,6 +81,47 @@ RAW_DHCP_RENEWAL = (
b"\x43\x37\x08\x01\x21\x03\x06\x1c\x33\x3a\x3b\xff" b"\x43\x37\x08\x01\x21\x03\x06\x1c\x33\x3a\x3b\xff"
) )
# <no hostname> 60:6b:bd:59:e4:b4 192.168.107.151
RAW_DHCP_REQUEST_WITHOUT_HOSTNAME = (
b"\xff\xff\xff\xff\xff\xff\x60\x6b\xbd\x59\xe4\xb4\x08\x00\x45\x00"
b"\x02\x40\x00\x00\x00\x00\x40\x11\x78\xae\x00\x00\x00\x00\xff\xff"
b"\xff\xff\x00\x44\x00\x43\x02\x2c\x02\x04\x01\x01\x06\x00\xff\x92"
b"\x7e\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x60\x6b\xbd\x59\xe4\xb4\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x03\x3d\x07\x01"
b"\x60\x6b\xbd\x59\xe4\xb4\x3c\x25\x75\x64\x68\x63\x70\x20\x31\x2e"
b"\x31\x34\x2e\x33\x2d\x56\x44\x20\x4c\x69\x6e\x75\x78\x20\x56\x44"
b"\x4c\x69\x6e\x75\x78\x2e\x31\x2e\x32\x2e\x31\x2e\x78\x32\x04\xc0"
b"\xa8\x6b\x97\x36\x04\xc0\xa8\x6b\x01\x37\x07\x01\x03\x06\x0c\x0f"
b"\x1c\x2a\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
async def test_dhcp_match_hostname_and_macaddress(hass): async def test_dhcp_match_hostname_and_macaddress(hass):
"""Test matching based on hostname and macaddress.""" """Test matching based on hostname and macaddress."""
@ -182,6 +223,29 @@ async def test_dhcp_match_macaddress(hass):
} }
async def test_dhcp_match_macaddress_without_hostname(hass):
"""Test matching based on macaddress only."""
dhcp_watcher = dhcp.DHCPWatcher(
hass, {}, [{"domain": "mock-domain", "macaddress": "606BBD*"}]
)
packet = Ether(RAW_DHCP_REQUEST_WITHOUT_HOSTNAME)
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_DHCP
}
assert mock_init.mock_calls[0][2]["data"] == {
dhcp.IP_ADDRESS: "192.168.107.151",
dhcp.HOSTNAME: "",
dhcp.MAC_ADDRESS: "606bbd59e4b4",
}
async def test_dhcp_nomatch(hass): async def test_dhcp_nomatch(hass):
"""Test not matching based on macaddress only.""" """Test not matching based on macaddress only."""
dhcp_watcher = dhcp.DHCPWatcher( dhcp_watcher = dhcp.DHCPWatcher(

View File

@ -146,7 +146,7 @@ async def test_plant_topology_reduction_change(
return_value=mock_modules, return_value=mock_modules,
) as mock_check: ) as mock_check:
async_fire_time_changed( async_fire_time_changed(
hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400)
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_check.mock_calls) == 1 assert len(mock_check.mock_calls) == 1
@ -208,7 +208,7 @@ async def test_plant_topology_increase_change(
return_value=mock_modules, return_value=mock_modules,
) as mock_check: ) as mock_check:
async_fire_time_changed( async_fire_time_changed(
hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400)
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_check.mock_calls) == 1 assert len(mock_check.mock_calls) == 1
@ -268,7 +268,7 @@ async def test_module_status_unavailable(hass, mock_config_entry, mock_modules):
return_value=mock_modules, return_value=mock_modules,
) as mock_check: ) as mock_check:
async_fire_time_changed( async_fire_time_changed(
hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400)
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_check.mock_calls) == 1 assert len(mock_check.mock_calls) == 1
@ -339,7 +339,7 @@ async def test_module_status_available(
return_value=mock_modules, return_value=mock_modules,
) as mock_check: ) as mock_check:
async_fire_time_changed( async_fire_time_changed(
hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400)
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_check.mock_calls) == 1 assert len(mock_check.mock_calls) == 1
@ -443,7 +443,7 @@ async def test_update_with_api_error(
side_effect=HomePlusControlApiError, side_effect=HomePlusControlApiError,
) as mock_check: ) as mock_check:
async_fire_time_changed( async_fire_time_changed(
hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400)
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_check.mock_calls) == 1 assert len(mock_check.mock_calls) == 1

View File

@ -1,6 +1,8 @@
"""Tests for Plex sensors.""" """Tests for Plex sensors."""
from datetime import timedelta from datetime import timedelta
import requests.exceptions
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -15,6 +17,7 @@ LIBRARY_UPDATE_PAYLOAD = {"StatusNotification": [{"title": "Library scan complet
async def test_library_sensor_values( async def test_library_sensor_values(
hass, hass,
caplog,
setup_plex_server, setup_plex_server,
mock_websocket, mock_websocket,
requests_mock, requests_mock,
@ -63,6 +66,34 @@ async def test_library_sensor_values(
assert library_tv_sensor.attributes["seasons"] == 1 assert library_tv_sensor.attributes["seasons"] == 1
assert library_tv_sensor.attributes["shows"] == 1 assert library_tv_sensor.attributes["shows"] == 1
# Handle `requests` exception
requests_mock.get(
"/library/sections/2/all?includeCollections=0&type=2",
exc=requests.exceptions.ReadTimeout,
)
trigger_plex_update(
mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD
)
await hass.async_block_till_done()
library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows")
assert library_tv_sensor.state == STATE_UNAVAILABLE
assert "Could not update library sensor" in caplog.text
# Ensure sensor updates properly when it recovers
requests_mock.get(
"/library/sections/2/all?includeCollections=0&type=2",
text=library_tvshows_size,
)
trigger_plex_update(
mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD
)
await hass.async_block_till_done()
library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows")
assert library_tv_sensor.state == "10"
# Handle library deletion # Handle library deletion
requests_mock.get( requests_mock.get(
"/library/sections/2/all?includeCollections=0&type=2", status_code=404 "/library/sections/2/all?includeCollections=0&type=2", status_code=404

View File

@ -3,7 +3,7 @@ import socket
from unittest.mock import Mock, PropertyMock, call, patch from unittest.mock import Mock, PropertyMock, call, patch
from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungctl.exceptions import AccessDenied, UnhandledResponse
from samsungtvws.exceptions import ConnectionFailure from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException, WebSocketProtocolException from websocket import WebSocketException, WebSocketProtocolException
from homeassistant import config_entries from homeassistant import config_entries
@ -86,6 +86,7 @@ MOCK_SSDP_DATA_WRONGMODEL = {
ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df",
} }
MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}
EXISTING_IP = "192.168.40.221"
MOCK_ZEROCONF_DATA = { MOCK_ZEROCONF_DATA = {
CONF_HOST: "fake_host", CONF_HOST: "fake_host",
CONF_PORT: 1234, CONF_PORT: 1234,
@ -99,7 +100,13 @@ MOCK_ZEROCONF_DATA = {
MOCK_OLD_ENTRY = { MOCK_OLD_ENTRY = {
CONF_HOST: "fake_host", CONF_HOST: "fake_host",
CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old",
CONF_IP_ADDRESS: "fake_ip_old", CONF_IP_ADDRESS: EXISTING_IP,
CONF_METHOD: "legacy",
CONF_PORT: None,
}
MOCK_LEGACY_ENTRY = {
CONF_HOST: EXISTING_IP,
CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old",
CONF_METHOD: "legacy", CONF_METHOD: "legacy",
CONF_PORT: None, CONF_PORT: None,
} }
@ -306,6 +313,11 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock):
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "confirm" assert result["step_id"] == "confirm"
with patch(
"homeassistant.components.samsungtv.bridge.Remote.__enter__",
return_value=True,
):
# entry was added # entry was added
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input="whatever" result["flow_id"], user_input="whatever"
@ -867,7 +879,7 @@ async def test_update_old_entry(hass: HomeAssistant, remote: Mock):
assert len(config_entries_domain) == 1 assert len(config_entries_domain) == 1
assert entry is config_entries_domain[0] assert entry is config_entries_domain[0]
assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old"
assert entry.data[CONF_IP_ADDRESS] == "fake_ip_old" assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP
assert not entry.unique_id assert not entry.unique_id
assert await async_setup_component(hass, DOMAIN, {}) is True assert await async_setup_component(hass, DOMAIN, {}) is True
@ -998,6 +1010,69 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock):
"""Test missing mac added."""
entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_LEGACY_ENTRY,
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.samsungtv.async_setup",
return_value=True,
) as mock_setup, patch(
"homeassistant.components.samsungtv.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"},
)
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mock):
"""Test missing mac added when there is no unique id."""
entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_LEGACY_ENTRY,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.samsungtv.bridge.SamsungTVWS.rest_device_info",
side_effect=HttpApiError,
), patch(
"homeassistant.components.samsungtv.bridge.Remote.__enter__",
return_value=True,
), patch(
"homeassistant.components.samsungtv.async_setup",
return_value=True,
) as mock_setup, patch(
"homeassistant.components.samsungtv.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"},
)
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert result["type"] == "abort"
assert result["reason"] == "not_supported"
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
assert entry.unique_id is None
async def test_form_reauth_legacy(hass, remote: Mock): async def test_form_reauth_legacy(hass, remote: Mock):
"""Test reauthenticate legacy.""" """Test reauthenticate legacy."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY)
@ -1068,9 +1143,6 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
import pprint
pprint.pprint(result2)
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": RESULT_AUTH_MISSING} assert result2["errors"] == {"base": RESULT_AUTH_MISSING}

View File

@ -3,8 +3,8 @@ import asyncio
import time import time
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
from zigpy.device import Device as zigpy_dev import zigpy.device as zigpy_dev
from zigpy.endpoint import Endpoint as zigpy_ep import zigpy.endpoint as zigpy_ep
import zigpy.profiles.zha import zigpy.profiles.zha
import zigpy.types import zigpy.types
import zigpy.zcl import zigpy.zcl
@ -27,7 +27,7 @@ class FakeEndpoint:
self.out_clusters = {} self.out_clusters = {}
self._cluster_attr = {} self._cluster_attr = {}
self.member_of = {} self.member_of = {}
self.status = 1 self.status = zigpy_ep.Status.ZDO_INIT
self.manufacturer = manufacturer self.manufacturer = manufacturer
self.model = model self.model = model
self.profile_id = zigpy.profiles.zha.PROFILE_ID self.profile_id = zigpy.profiles.zha.PROFILE_ID
@ -57,7 +57,7 @@ class FakeEndpoint:
@property @property
def __class__(self): def __class__(self):
"""Fake being Zigpy endpoint.""" """Fake being Zigpy endpoint."""
return zigpy_ep return zigpy_ep.Endpoint
@property @property
def unique_id(self): def unique_id(self):
@ -65,8 +65,8 @@ class FakeEndpoint:
return self.device.ieee, self.endpoint_id return self.device.ieee, self.endpoint_id
FakeEndpoint.add_to_group = zigpy_ep.add_to_group FakeEndpoint.add_to_group = zigpy_ep.Endpoint.add_to_group
FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group FakeEndpoint.remove_from_group = zigpy_ep.Endpoint.remove_from_group
def patch_cluster(cluster): def patch_cluster(cluster):
@ -125,12 +125,11 @@ class FakeDevice:
self.lqi = 255 self.lqi = 255
self.rssi = 8 self.rssi = 8
self.last_seen = time.time() self.last_seen = time.time()
self.status = 2 self.status = zigpy_dev.Status.ENDPOINTS_INIT
self.initializing = False self.initializing = False
self.skip_configuration = False self.skip_configuration = False
self.manufacturer = manufacturer self.manufacturer = manufacturer
self.model = model self.model = model
self.node_desc = zigpy.zdo.types.NodeDescriptor()
self.remove_from_group = AsyncMock() self.remove_from_group = AsyncMock()
if node_desc is None: if node_desc is None:
node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00"
@ -138,7 +137,7 @@ class FakeDevice:
self.neighbors = [] self.neighbors = []
FakeDevice.add_to_group = zigpy_dev.add_to_group FakeDevice.add_to_group = zigpy_dev.Device.add_to_group
def get_zha_gateway(hass): def get_zha_gateway(hass):