mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
commit
bcab1414f8
@ -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
|
||||
|
@ -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"],
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -18,7 +18,11 @@
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "tizen*"
|
||||
}
|
||||
},
|
||||
{"macaddress": "8CC8CD*"},
|
||||
{"macaddress": "606BBD*"},
|
||||
{"macaddress": "F47B5E*"},
|
||||
{"macaddress": "4844F7*"}
|
||||
],
|
||||
"codeowners": [
|
||||
"@escoand",
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -266,6 +266,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
"manufacturer": "Spotify AB",
|
||||
"model": model,
|
||||
"name": self._name,
|
||||
"entry_type": "service",
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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: *",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user