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/mjpeg/camera.py
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/modbus.py
homeassistant/components/modem_callerid/sensor.py

View File

@ -3,7 +3,7 @@
"name": "Apple TV",
"config_flow": true,
"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."],
"after_dependencies": ["discovery"],
"codeowners": ["@postlund"],

View File

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

View File

@ -248,10 +248,10 @@ class DeviceTrackerWatcher(WatcherBase):
return
ip_address = attributes.get(ATTR_IP)
hostname = attributes.get(ATTR_HOST_NAME)
hostname = attributes.get(ATTR_HOST_NAME, "")
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
self.process_client(ip_address, hostname, _format_mac(mac_address))
@ -328,10 +328,10 @@ class DHCPWatcher(WatcherBase):
return
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)
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
self.process_client(ip_address, hostname, mac_address)

View File

@ -3,7 +3,7 @@
"name": "FireServiceRota",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
"requirements": ["pyfireservicerota==0.0.42"],
"requirements": ["pyfireservicerota==0.0.43"],
"codeowners": ["@cyberjunky"],
"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",
update_method=async_update_data,
# 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

View File

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

View File

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

View File

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

View File

@ -265,6 +265,17 @@ turn_on:
min: -100
max: 100
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:
name: Profile
description: Name of a light profile to use.

View File

@ -51,6 +51,7 @@ class BasePlatform(Entity):
self._value = None
self._available = True
self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
self._call_active = False
@abstractmethod
async def async_update(self, now=None):
@ -160,9 +161,14 @@ class BaseSwitch(BasePlatform, RestoreEntity):
self.async_write_ha_state()
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(
self._slave, self._verify_address, 1, self._verify_type
)
self._call_active = False
if result is None:
self._available = False
self.async_write_ha_state()

View File

@ -54,9 +54,15 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
async def async_update(self, now=None):
"""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(
self._slave, self._address, 1, self._input_type
)
self._call_active = False
if result is None:
self._available = False
self.async_write_ha_state()

View File

@ -185,13 +185,18 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity):
"""Update Target & Current Temperature."""
# remark "now" is a dummy parameter to avoid problems with
# 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(
CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
)
self._current_temperature = await self._async_read_register(
self._input_type, self._address
)
self._call_active = False
self.async_write_ha_state()
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."""
# remark "now" is a dummy parameter to avoid problems with
# 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(
self._slave, self._address, 1, self._input_type
)
self._call_active = False
if result is None:
self._available = False
self.async_write_ha_state()

View File

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

View File

@ -2,6 +2,7 @@
import logging
from plexapi.exceptions import NotFound
import requests.exceptions
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.debounce import Debouncer
@ -171,6 +172,13 @@ class PlexLibrarySectionSensor(SensorEntity):
self._available = True
except NotFound:
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()
def _update_state_and_attrs(self):

View File

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

View File

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

View File

@ -6,6 +6,8 @@ from typing import Any
from pysiaalarm import SIAEvent
from homeassistant.util.dt import utcnow
from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE
PING_INTERVAL_MARGIN = 30
@ -23,7 +25,9 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]:
ATTR_CODE: event.code,
ATTR_MESSAGE: event.message,
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,
"message": event.message,
"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_type": event.event_type,
"partition": event.partition,

View File

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

View File

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

View File

@ -183,11 +183,12 @@ class ZHADevice(LogMixin):
return self._zigpy_device.model
@property
def manufacturer_code(self):
def manufacturer_code(self) -> int | None:
"""Return the manufacturer code for the device."""
if self._zigpy_device.node_desc.is_valid:
return self._zigpy_device.node_desc.manufacturer_code
return None
if self._zigpy_device.node_desc is None:
return None
return self._zigpy_device.node_desc.manufacturer_code
@property
def nwk(self):
@ -210,17 +211,20 @@ class ZHADevice(LogMixin):
return self._zigpy_device.last_seen
@property
def is_mains_powered(self):
def is_mains_powered(self) -> bool | None:
"""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
@property
def device_type(self):
def device_type(self) -> str:
"""Return the logical device type for the device."""
node_descriptor = self._zigpy_device.node_desc
return (
node_descriptor.logical_type.name if node_descriptor.is_valid else UNKNOWN
)
if self._zigpy_device.node_desc is None:
return UNKNOWN
return self._zigpy_device.node_desc.logical_type.name
@property
def power_source(self):
@ -230,18 +234,27 @@ class ZHADevice(LogMixin):
)
@property
def is_router(self):
def is_router(self) -> bool | None:
"""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
@property
def is_coordinator(self):
def is_coordinator(self) -> bool | None:
"""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
@property
def is_end_device(self):
def is_end_device(self) -> bool | None:
"""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
@property

View File

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

View File

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

View File

@ -1318,7 +1318,7 @@ pyatmo==5.2.0
pyatome==0.1.1
# homeassistant.components.apple_tv
pyatv==0.7.7
pyatv==0.8.1
# homeassistant.components.bbox
pybbox==0.0.5-alpha
@ -1423,7 +1423,7 @@ pyezviz==0.1.8.9
pyfido==2.1.1
# homeassistant.components.fireservicerota
pyfireservicerota==0.0.42
pyfireservicerota==0.0.43
# homeassistant.components.flexit
pyflexit==0.3
@ -1490,7 +1490,7 @@ pyialarm==1.9.0
pyicloud==0.10.2
# homeassistant.components.insteon
pyinsteon==1.0.11
pyinsteon==1.0.12
# homeassistant.components.intesishome
pyintesishome==1.7.6
@ -1669,7 +1669,7 @@ pyownet==0.10.0.post1
pypca==0.0.7
# homeassistant.components.lcn
pypck==0.7.9
pypck==0.7.10
# homeassistant.components.pjlink
pypjlink2==1.2.1
@ -1696,7 +1696,7 @@ pyqwikswitch==0.93
pyrail==0.0.3
# homeassistant.components.rainbird
pyrainbird==0.4.2
pyrainbird==0.4.3
# homeassistant.components.recswitch
pyrecswitch==1.0.2
@ -1749,7 +1749,7 @@ pysignalclirestapi==0.3.4
pyskyqhub==0.1.3
# homeassistant.components.sma
pysma==0.6.2
pysma==0.6.4
# homeassistant.components.smappee
pysmappee==0.2.25

View File

@ -743,7 +743,7 @@ pyatag==0.3.5.3
pyatmo==5.2.0
# homeassistant.components.apple_tv
pyatv==0.7.7
pyatv==0.8.1
# homeassistant.components.blackbird
pyblackbird==0.5
@ -791,7 +791,7 @@ pyezviz==0.1.8.9
pyfido==2.1.1
# homeassistant.components.fireservicerota
pyfireservicerota==0.0.42
pyfireservicerota==0.0.43
# homeassistant.components.flume
pyflume==0.5.5
@ -837,7 +837,7 @@ pyialarm==1.9.0
pyicloud==0.10.2
# homeassistant.components.insteon
pyinsteon==1.0.11
pyinsteon==1.0.12
# homeassistant.components.ipma
pyipma==2.0.5
@ -950,7 +950,7 @@ pyowm==3.2.0
pyownet==0.10.0.post1
# homeassistant.components.lcn
pypck==0.7.9
pypck==0.7.10
# homeassistant.components.plaato
pyplaato==0.0.15
@ -991,7 +991,7 @@ pysiaalarm==3.0.0
pysignalclirestapi==0.3.4
# homeassistant.components.sma
pysma==0.6.2
pysma==0.6.4
# homeassistant.components.smappee
pysmappee==0.2.25

View File

@ -2,7 +2,8 @@
from unittest.mock import patch
from pyatv import conf, net
from pyatv import conf
from pyatv.support.http import create_session
import pytest
from .common import MockPairingHandler, create_conf
@ -39,7 +40,7 @@ def pairing():
async def _pair(config, protocol, loop, session=None, **kwargs):
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
return handler
@ -121,11 +122,7 @@ def dmap_device_with_credentials(mock_scan):
@pytest.fixture
def airplay_device(mock_scan):
def device_with_no_services(mock_scan):
"""Mock pyatv.scan."""
mock_scan.result.append(
create_conf(
"127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777)
)
)
mock_scan.result.append(create_conf("127.0.0.1", "Invalid Device"))
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"}
async def test_user_adds_unusable_device(hass, airplay_device):
"""Test that it is not possible to add pure AirPlay device."""
async def test_user_adds_unusable_device(hass, device_with_no_services):
"""Test that it is not possible to add device with no services."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device_input": "AirPlay Device"},
{"device_input": "Invalid Device"},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
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"
)
# <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):
"""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):
"""Test not matching based on macaddress only."""
dhcp_watcher = dhcp.DHCPWatcher(

View File

@ -146,7 +146,7 @@ async def test_plant_topology_reduction_change(
return_value=mock_modules,
) as mock_check:
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()
assert len(mock_check.mock_calls) == 1
@ -208,7 +208,7 @@ async def test_plant_topology_increase_change(
return_value=mock_modules,
) as mock_check:
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()
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,
) as mock_check:
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()
assert len(mock_check.mock_calls) == 1
@ -339,7 +339,7 @@ async def test_module_status_available(
return_value=mock_modules,
) as mock_check:
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()
assert len(mock_check.mock_calls) == 1
@ -443,7 +443,7 @@ async def test_update_with_api_error(
side_effect=HomePlusControlApiError,
) as mock_check:
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()
assert len(mock_check.mock_calls) == 1

View File

@ -1,6 +1,8 @@
"""Tests for Plex sensors."""
from datetime import timedelta
import requests.exceptions
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_UNAVAILABLE
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(
hass,
caplog,
setup_plex_server,
mock_websocket,
requests_mock,
@ -63,6 +66,34 @@ async def test_library_sensor_values(
assert library_tv_sensor.attributes["seasons"] == 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
requests_mock.get(
"/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 samsungctl.exceptions import AccessDenied, UnhandledResponse
from samsungtvws.exceptions import ConnectionFailure
from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException, WebSocketProtocolException
from homeassistant import config_entries
@ -86,6 +86,7 @@ MOCK_SSDP_DATA_WRONGMODEL = {
ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df",
}
MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}
EXISTING_IP = "192.168.40.221"
MOCK_ZEROCONF_DATA = {
CONF_HOST: "fake_host",
CONF_PORT: 1234,
@ -99,7 +100,13 @@ MOCK_ZEROCONF_DATA = {
MOCK_OLD_ENTRY = {
CONF_HOST: "fake_host",
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_PORT: None,
}
@ -306,17 +313,22 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock):
assert result["type"] == "form"
assert result["step_id"] == "confirm"
# entry was added
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input="whatever"
)
assert result["type"] == "create_entry"
assert result["title"] == "fake2_model"
assert result["data"][CONF_HOST] == "fake2_host"
assert result["data"][CONF_NAME] == "fake2_model"
assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer"
assert result["data"][CONF_MODEL] == "fake2_model"
assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df"
with patch(
"homeassistant.components.samsungtv.bridge.Remote.__enter__",
return_value=True,
):
# entry was added
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input="whatever"
)
assert result["type"] == "create_entry"
assert result["title"] == "fake2_model"
assert result["data"][CONF_HOST] == "fake2_host"
assert result["data"][CONF_NAME] == "fake2_model"
assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer"
assert result["data"][CONF_MODEL] == "fake2_model"
assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df"
async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock):
@ -867,7 +879,7 @@ async def test_update_old_entry(hass: HomeAssistant, remote: Mock):
assert len(config_entries_domain) == 1
assert entry is config_entries_domain[0]
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 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"
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):
"""Test reauthenticate legacy."""
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()
import pprint
pprint.pprint(result2)
assert result2["type"] == "form"
assert result2["errors"] == {"base": RESULT_AUTH_MISSING}

View File

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