Merge pull request #59397 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-11-08 21:44:29 -08:00 committed by GitHub
commit 435f278053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 155 additions and 114 deletions

View File

@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome", "name": "Flux LED/MagicHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led", "documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.24.14"], "requirements": ["flux_led==0.24.17"],
"quality_scale": "platinum", "quality_scale": "platinum",
"codeowners": ["@icemanch"], "codeowners": ["@icemanch"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -370,7 +370,7 @@ class FritzBoxTools:
device_reg = async_get(self.hass) device_reg = async_get(self.hass)
device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id) device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id)
for device_entry in device_list: for device_entry in device_list:
if async_entries_for_device( if not async_entries_for_device(
entity_reg, entity_reg,
device_entry.id, device_entry.id,
include_disabled_entities=True, include_disabled_entities=True,

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [ "requirements": [
"home-assistant-frontend==20211103.0" "home-assistant-frontend==20211108.0"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

@ -3,7 +3,7 @@
"name": "Elexa Guardian", "name": "Elexa Guardian",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/guardian", "documentation": "https://www.home-assistant.io/integrations/guardian",
"requirements": ["aioguardian==1.0.8"], "requirements": ["aioguardian==2021.11.0"],
"zeroconf": ["_api._udp.local."], "zeroconf": ["_api._udp.local."],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -3,7 +3,7 @@
"name": "MQTT", "name": "MQTT",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mqtt", "documentation": "https://www.home-assistant.io/integrations/mqtt",
"requirements": ["paho-mqtt==1.5.1"], "requirements": ["paho-mqtt==1.6.1"],
"dependencies": ["http"], "dependencies": ["http"],
"codeowners": ["@emontnemery"], "codeowners": ["@emontnemery"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -1,17 +1,11 @@
"""Support for ReCollect Waste sensors.""" """Support for ReCollect Waste sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import date, datetime, time
from aiorecollect.client import PickupType from aiorecollect.client import PickupType
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE
ATTR_ATTRIBUTION,
CONF_FRIENDLY_NAME,
DEVICE_CLASS_TIMESTAMP,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -19,7 +13,6 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from homeassistant.util.dt import as_utc
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN
@ -47,12 +40,6 @@ def async_get_pickup_type_names(
] ]
@callback
def async_get_utc_midnight(target_date: date) -> datetime:
"""Get UTC midnight for a given date."""
return as_utc(datetime.combine(target_date, time(0)))
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
@ -64,7 +51,7 @@ async def async_setup_entry(
class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): class ReCollectWasteSensor(CoordinatorEntity, SensorEntity):
"""ReCollect Waste Sensor.""" """ReCollect Waste Sensor."""
_attr_device_class = DEVICE_CLASS_TIMESTAMP _attr_device_class = DEVICE_CLASS_DATE
def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
@ -91,8 +78,13 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity):
@callback @callback
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update the state.""" """Update the state."""
pickup_event = self.coordinator.data[0] try:
next_pickup_event = self.coordinator.data[1] pickup_event = self.coordinator.data[0]
next_pickup_event = self.coordinator.data[1]
except IndexError:
self._attr_native_value = None
self._attr_extra_state_attributes = {}
return
self._attr_extra_state_attributes.update( self._attr_extra_state_attributes.update(
{ {
@ -103,9 +95,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity):
ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names(
self._entry, next_pickup_event.pickup_types self._entry, next_pickup_event.pickup_types
), ),
ATTR_NEXT_PICKUP_DATE: async_get_utc_midnight( ATTR_NEXT_PICKUP_DATE: next_pickup_event.date.isoformat(),
next_pickup_event.date
).isoformat(),
} }
) )
self._attr_native_value = async_get_utc_midnight(pickup_event.date).isoformat() self._attr_native_value = pickup_event.date.isoformat()

View File

@ -282,9 +282,6 @@ class ShellyBlockEntity(entity.Entity):
self.wrapper = wrapper self.wrapper = wrapper
self.block = block self.block = block
self._name = get_block_entity_name(wrapper.device, block) self._name = get_block_entity_name(wrapper.device, block)
self._attr_device_info = DeviceInfo(
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
)
@property @property
def name(self) -> str: def name(self) -> str:
@ -296,6 +293,13 @@ class ShellyBlockEntity(entity.Entity):
"""If device should be polled.""" """If device should be polled."""
return False return False
@property
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Available.""" """Available."""
@ -344,9 +348,9 @@ class ShellyRpcEntity(entity.Entity):
self.wrapper = wrapper self.wrapper = wrapper
self.key = key self.key = key
self._attr_should_poll = False self._attr_should_poll = False
self._attr_device_info = DeviceInfo( self._attr_device_info = {
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} "connections": {(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
) }
self._attr_unique_id = f"{wrapper.mac}-{key}" self._attr_unique_id = f"{wrapper.mac}-{key}"
self._attr_name = get_rpc_entity_name(wrapper.device, key) self._attr_name = get_rpc_entity_name(wrapper.device, key)
@ -490,15 +494,19 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
self.description = description self.description = description
self._name = get_block_entity_name(wrapper.device, None, self.description.name) self._name = get_block_entity_name(wrapper.device, None, self.description.name)
self._last_value = None self._last_value = None
self._attr_device_info = DeviceInfo(
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
)
@property @property
def name(self) -> str: def name(self) -> str:
"""Name of sensor.""" """Name of sensor."""
return self._name return self._name
@property
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}
@property @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:
"""Return if it should be enabled by default.""" """Return if it should be enabled by default."""

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==1.0.2"], "requirements": ["aioshelly==1.0.4"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -2,7 +2,7 @@
"domain": "shiftr", "domain": "shiftr",
"name": "shiftr.io", "name": "shiftr.io",
"documentation": "https://www.home-assistant.io/integrations/shiftr", "documentation": "https://www.home-assistant.io/integrations/shiftr",
"requirements": ["paho-mqtt==1.5.1"], "requirements": ["paho-mqtt==1.6.1"],
"codeowners": ["@fabaff"], "codeowners": ["@fabaff"],
"iot_class": "cloud_push" "iot_class": "cloud_push"
} }

View File

@ -103,9 +103,11 @@ ATTR_TIMESTAMP = "timestamp"
DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_MODEL = "alarm_control_panel"
DEFAULT_ENTITY_NAME = "Alarm Control Panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel"
DEFAULT_REST_API_ERROR_COUNT = 2
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
DEFAULT_SOCKET_MIN_RETRY = 15 DEFAULT_SOCKET_MIN_RETRY = 15
DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}"
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
@ -556,6 +558,8 @@ class SimpliSafeEntity(CoordinatorEntity):
assert simplisafe.coordinator assert simplisafe.coordinator
super().__init__(simplisafe.coordinator) super().__init__(simplisafe.coordinator)
self._rest_api_errors = 0
if device: if device:
model = device.type.name model = device.type.name
device_name = device.name device_name = device.name
@ -618,11 +622,24 @@ class SimpliSafeEntity(CoordinatorEntity):
else: else:
system_offline = False system_offline = False
return super().available and self._online and not system_offline return (
self._rest_api_errors < DEFAULT_REST_API_ERROR_COUNT
and self._online
and not system_offline
)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Update the entity with new REST API data.""" """Update the entity with new REST API data."""
# SimpliSafe can incorrectly return an error state when there isn't any
# error. This can lead to the system having an unknown state frequently.
# To protect against that, we measure how many "error states" we receive
# and only alter the state if we detect a few in a row:
if self.coordinator.last_update_success:
self._rest_api_errors = 0
else:
self._rest_api_errors += 1
self.async_update_from_rest_api() self.async_update_from_rest_api()
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -72,8 +72,6 @@ ATTR_RF_JAMMING = "rf_jamming"
ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WALL_POWER_LEVEL = "wall_power_level"
ATTR_WIFI_STRENGTH = "wifi_strength" ATTR_WIFI_STRENGTH = "wifi_strength"
DEFAULT_ERRORS_TO_ACCOMMODATE = 2
VOLUME_STRING_MAP = { VOLUME_STRING_MAP = {
VOLUME_HIGH: "high", VOLUME_HIGH: "high",
VOLUME_LOW: "low", VOLUME_LOW: "low",
@ -141,8 +139,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR,
) )
self._errors = 0
if code := self._simplisafe.entry.options.get(CONF_CODE): if code := self._simplisafe.entry.options.get(CONF_CODE):
if code.isdigit(): if code.isdigit():
self._attr_code_format = FORMAT_NUMBER self._attr_code_format = FORMAT_NUMBER
@ -249,19 +245,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
} }
) )
# SimpliSafe can incorrectly return an error state when there isn't any
# error. This can lead to the system having an unknown state frequently.
# To protect against that, we measure how many "error states" we receive
# and only alter the state if we detect a few in a row:
if self._system.state == SystemStates.error:
if self._errors > DEFAULT_ERRORS_TO_ACCOMMODATE:
self._attr_state = None
else:
self._errors += 1
return
self._errors = 0
self._set_state_from_system_data() self._set_state_from_system_data()
@callback @callback

View File

@ -102,18 +102,18 @@ class SegmentBuffer:
# The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x] # The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x]
# of the part target duration. We use the frag_duration option to tell ffmpeg to try to # of the part target duration. We use the frag_duration option to tell ffmpeg to try to
# cut the fragments when they reach frag_duration. However, the resulting fragments can # cut the fragments when they reach frag_duration. However, the resulting fragments can
# have variability in their durations and can end up being too short or too long. If # have variability in their durations and can end up being too short or too long. With a
# there are two tracks, as in the case of a video feed with audio, the fragment cut seems
# to be done on the first track that crosses the desired threshold, and cutting on the
# audio track may result in a shorter video fragment than desired. Conversely, with a
# video track with no audio, the discrete nature of frames means that the frame at the # video track with no audio, the discrete nature of frames means that the frame at the
# end of a fragment will sometimes extend slightly beyond the desired frag_duration. # end of a fragment will sometimes extend slightly beyond the desired frag_duration.
# Given this, our approach is to use a frag_duration near the upper end of the range for # If there are two tracks, as in the case of a video feed with audio, there is an added
# outputs with audio using a frag_duration at the lower end of the range for outputs with # wrinkle as the fragment cut seems to be done on the first track that crosses the desired
# only video. # threshold, and cutting on the audio track may also result in a shorter video fragment
# than desired.
# Given this, our approach is to give ffmpeg a frag_duration somewhere in the middle
# of the range, hoping that the parts stay pretty well bounded, and we adjust the part
# durations a bit in the hls metadata so that everything "looks" ok.
"frag_duration": str( "frag_duration": str(
self._stream_settings.part_target_duration self._stream_settings.part_target_duration * 9e5
* (98e4 if add_audio else 9e5)
), ),
} }
if self._stream_settings.ll_hls if self._stream_settings.ll_hls

View File

@ -233,7 +233,7 @@ async def async_setup_entry( # noqa: C901
surveillance_station = api.surveillance_station surveillance_station = api.surveillance_station
try: try:
async with async_timeout.timeout(10): async with async_timeout.timeout(30):
await hass.async_add_executor_job(surveillance_station.update) await hass.async_add_executor_job(surveillance_station.update)
except SynologyDSMAPIErrorException as err: except SynologyDSMAPIErrorException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err raise UpdateFailed(f"Error communicating with API: {err}") from err

View File

@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
TotalConnectClient, username, password, usercodes TotalConnectClient, username, password, usercodes
) )
if not client.is_valid_credentials(): if not client.is_logged_in():
raise ConfigEntryAuthFailed("TotalConnect authentication failed") raise ConfigEntryAuthFailed("TotalConnect authentication failed")
coordinator = TotalConnectDataUpdateCoordinator(hass, client) coordinator = TotalConnectDataUpdateCoordinator(hass, client)
@ -88,5 +88,3 @@ class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator):
raise UpdateFailed(exception) from exception raise UpdateFailed(exception) from exception
except ValueError as exception: except ValueError as exception:
raise UpdateFailed("Unknown state from TotalConnect") from exception raise UpdateFailed("Unknown state from TotalConnect") from exception
return True

View File

@ -40,7 +40,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
TotalConnectClient, username, password, None TotalConnectClient, username, password, None
) )
if client.is_valid_credentials(): if client.is_logged_in():
# username/password valid so show user locations # username/password valid so show user locations
self.username = username self.username = username
self.password = password self.password = password
@ -136,7 +136,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.usercodes, self.usercodes,
) )
if not client.is_valid_credentials(): if not client.is_logged_in():
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
return self.async_show_form( return self.async_show_form(
step_id="reauth_confirm", step_id="reauth_confirm",

View File

@ -2,7 +2,7 @@
"domain": "totalconnect", "domain": "totalconnect",
"name": "Total Connect", "name": "Total Connect",
"documentation": "https://www.home-assistant.io/integrations/totalconnect", "documentation": "https://www.home-assistant.io/integrations/totalconnect",
"requirements": ["total_connect_client==2021.8.3"], "requirements": ["total_connect_client==2021.11.2"],
"dependencies": [], "dependencies": [],
"codeowners": ["@austinmroczek"], "codeowners": ["@austinmroczek"],
"config_flow": true, "config_flow": true,

View File

@ -60,7 +60,6 @@ class TradfriBaseClass(Entity):
"""Initialize a device.""" """Initialize a device."""
self._api = handle_error(api) self._api = handle_error(api)
self._attr_name = device.name self._attr_name = device.name
self._attr_available = device.reachable
self._device: Device = device self._device: Device = device
self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = (
None None
@ -105,7 +104,6 @@ class TradfriBaseClass(Entity):
"""Refresh the device data.""" """Refresh the device data."""
self._device = device self._device = device
self._attr_name = device.name self._attr_name = device.name
self._attr_available = device.reachable
if write_ha: if write_ha:
self.async_write_ha_state() self.async_write_ha_state()
@ -116,6 +114,16 @@ class TradfriBaseDevice(TradfriBaseClass):
All devices should inherit from this class. All devices should inherit from this class.
""" """
def __init__(
self,
device: Device,
api: Callable[[Command | list[Command]], Any],
gateway_id: str,
) -> None:
"""Initialize a device."""
self._attr_available = device.reachable
super().__init__(device, api, gateway_id)
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
@ -128,3 +136,11 @@ class TradfriBaseDevice(TradfriBaseClass):
sw_version=info.firmware_version, sw_version=info.firmware_version,
via_device=(DOMAIN, self._gateway_id), via_device=(DOMAIN, self._gateway_id),
) )
def _refresh(self, device: Device, write_ha: bool = True) -> None:
"""Refresh the device data."""
# The base class _refresh cannot be used, because
# there are devices (group) that do not have .reachable
# so set _attr_available here and let the base class do the rest.
self._attr_available = device.reachable
super()._refresh(device, write_ha)

View File

@ -2,7 +2,7 @@
"domain": "velbus", "domain": "velbus",
"name": "Velbus", "name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus", "documentation": "https://www.home-assistant.io/integrations/velbus",
"requirements": ["velbus-aio==2021.11.0"], "requirements": ["velbus-aio==2021.11.6"],
"config_flow": true, "config_flow": true,
"codeowners": ["@Cereal2nd", "@brefra"], "codeowners": ["@Cereal2nd", "@brefra"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -66,6 +66,8 @@ from .const import (
MODELS_PURIFIER_MIOT, MODELS_PURIFIER_MIOT,
MODELS_SWITCH, MODELS_SWITCH,
MODELS_VACUUM, MODELS_VACUUM,
ROBOROCK_GENERIC,
ROCKROBO_GENERIC,
AuthException, AuthException,
SetupException, SetupException,
) )
@ -267,7 +269,7 @@ async def async_create_miio_device_and_coordinator(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry hass: core.HomeAssistant, entry: config_entries.ConfigEntry
): ):
"""Set up a data coordinator and one miio device to service multiple entities.""" """Set up a data coordinator and one miio device to service multiple entities."""
model = entry.data[CONF_MODEL] model: str = entry.data[CONF_MODEL]
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
token = entry.data[CONF_TOKEN] token = entry.data[CONF_TOKEN]
name = entry.title name = entry.title
@ -280,6 +282,8 @@ async def async_create_miio_device_and_coordinator(
model not in MODELS_HUMIDIFIER model not in MODELS_HUMIDIFIER
and model not in MODELS_FAN and model not in MODELS_FAN
and model not in MODELS_VACUUM and model not in MODELS_VACUUM
and not model.startswith(ROBOROCK_GENERIC)
and not model.startswith(ROCKROBO_GENERIC)
): ):
return return
@ -304,7 +308,11 @@ async def async_create_miio_device_and_coordinator(
device = AirPurifier(host, token) device = AirPurifier(host, token)
elif model.startswith("zhimi.airfresh."): elif model.startswith("zhimi.airfresh."):
device = AirFresh(host, token) device = AirFresh(host, token)
elif model in MODELS_VACUUM: elif (
model in MODELS_VACUUM
or model.startswith(ROBOROCK_GENERIC)
or model.startswith(ROCKROBO_GENERIC)
):
device = Vacuum(host, token) device = Vacuum(host, token)
update_method = _async_update_data_vacuum update_method = _async_update_data_vacuum
coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData] coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData]

View File

@ -202,7 +202,8 @@ ROCKROBO_S4_MAX = "roborock.vacuum.a19"
ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_S5_MAX = "roborock.vacuum.s5e"
ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_S6_PURE = "roborock.vacuum.a08"
ROCKROBO_E2 = "roborock.vacuum.e2" ROCKROBO_E2 = "roborock.vacuum.e2"
ROCKROBO_GENERIC = "roborock.vacuum" ROBOROCK_GENERIC = "roborock.vacuum"
ROCKROBO_GENERIC = "rockrobo.vacuum"
MODELS_VACUUM = [ MODELS_VACUUM = [
ROCKROBO_V1, ROCKROBO_V1,
ROCKROBO_E2, ROCKROBO_E2,
@ -214,6 +215,7 @@ MODELS_VACUUM = [
ROCKROBO_S6_MAXV, ROCKROBO_S6_MAXV,
ROCKROBO_S6_PURE, ROCKROBO_S6_PURE,
ROCKROBO_S7, ROCKROBO_S7,
ROBOROCK_GENERIC,
ROCKROBO_GENERIC, ROCKROBO_GENERIC,
] ]
MODELS_VACUUM_WITH_MOP = [ MODELS_VACUUM_WITH_MOP = [

View File

@ -81,6 +81,8 @@ from .const import (
MODELS_PURIFIER_MIIO, MODELS_PURIFIER_MIIO,
MODELS_PURIFIER_MIOT, MODELS_PURIFIER_MIOT,
MODELS_VACUUM, MODELS_VACUUM,
ROBOROCK_GENERIC,
ROCKROBO_GENERIC,
) )
from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice from .gateway import XiaomiGatewayDevice
@ -374,7 +376,6 @@ AIRFRESH_SENSORS = (
ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_LIFE_REMAINING,
ATTR_FILTER_USE, ATTR_FILTER_USE,
ATTR_HUMIDITY, ATTR_HUMIDITY,
ATTR_ILLUMINANCE_LUX,
ATTR_PM25, ATTR_PM25,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_USE_TIME, ATTR_USE_TIME,
@ -593,7 +594,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
host = config_entry.data[CONF_HOST] host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN] token = config_entry.data[CONF_TOKEN]
model = config_entry.data[CONF_MODEL] model: str = config_entry.data[CONF_MODEL]
if model in (MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5): if model in (MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5):
return return
@ -625,7 +626,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sensors = PURIFIER_MIIO_SENSORS sensors = PURIFIER_MIIO_SENSORS
elif model in MODELS_PURIFIER_MIOT: elif model in MODELS_PURIFIER_MIOT:
sensors = PURIFIER_MIOT_SENSORS sensors = PURIFIER_MIOT_SENSORS
elif model in MODELS_VACUUM: elif (
model in MODELS_VACUUM
or model.startswith(ROBOROCK_GENERIC)
or model.startswith(ROCKROBO_GENERIC)
):
return _setup_vacuum_sensors(hass, config_entry, async_add_entities) return _setup_vacuum_sensors(hass, config_entry, async_add_entities)
for sensor, description in SENSOR_TYPES.items(): for sensor, description in SENSOR_TYPES.items():

View File

@ -226,6 +226,22 @@ class Battery(Sensor):
_unit = PERCENTAGE _unit = PERCENTAGE
_attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZhaDeviceType,
channels: list[ChannelType],
**kwargs,
) -> ZhaEntity | None:
"""Entity Factory.
Unlike any other entity, PowerConfiguration cluster may not support
battery_percent_remaining attribute, but zha-device-handlers takes care of it
so create the entity regardless
"""
return cls(unique_id, zha_device, channels, **kwargs)
@staticmethod @staticmethod
def formatter(value: int) -> int: def formatter(value: int) -> int:
"""Return the state of the entity.""" """Return the state of the entity."""

View File

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

View File

@ -15,11 +15,11 @@ ciso8601==2.2.0
cryptography==3.4.8 cryptography==3.4.8
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.50.0
home-assistant-frontend==20211103.0 home-assistant-frontend==20211108.0
httpx==0.19.0 httpx==0.19.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.2 jinja2==3.0.2
paho-mqtt==1.5.1 paho-mqtt==1.6.1
pillow==8.2.0 pillow==8.2.0
pip>=8.0.3,<20.3 pip>=8.0.3,<20.3
pyserial==3.5 pyserial==3.5

View File

@ -173,7 +173,7 @@ aioftp==0.12.0
aiogithubapi==21.8.0 aiogithubapi==21.8.0
# homeassistant.components.guardian # homeassistant.components.guardian
aioguardian==1.0.8 aioguardian==2021.11.0
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.8 aioharmony==0.2.8
@ -243,7 +243,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==1.0.2 aioshelly==1.0.4
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==2.0.6 aioswitcher==2.0.6
@ -652,7 +652,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.24.14 flux_led==0.24.17
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -813,7 +813,7 @@ hole==0.5.1
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211103.0 home-assistant-frontend==20211108.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -1163,7 +1163,7 @@ p1monitor==1.0.0
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
paho-mqtt==1.5.1 paho-mqtt==1.6.1
# homeassistant.components.panasonic_bluray # homeassistant.components.panasonic_bluray
panacotta==0.1 panacotta==0.1
@ -2314,7 +2314,7 @@ todoist-python==8.0.0
toonapi==0.2.1 toonapi==0.2.1
# homeassistant.components.totalconnect # homeassistant.components.totalconnect
total_connect_client==2021.8.3 total_connect_client==2021.11.2
# homeassistant.components.tplink_lte # homeassistant.components.tplink_lte
tp-connected==0.0.4 tp-connected==0.0.4
@ -2360,7 +2360,7 @@ uvcclient==0.11.0
vallox-websocket-api==2.8.1 vallox-websocket-api==2.8.1
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2021.11.0 velbus-aio==2021.11.6
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.14 venstarcolortouch==0.14

View File

@ -115,7 +115,7 @@ aioesphomeapi==10.2.0
aioflo==0.4.1 aioflo==0.4.1
# homeassistant.components.guardian # homeassistant.components.guardian
aioguardian==1.0.8 aioguardian==2021.11.0
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.8 aioharmony==0.2.8
@ -170,7 +170,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==1.0.2 aioshelly==1.0.4
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==2.0.6 aioswitcher==2.0.6
@ -387,7 +387,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.24.14 flux_led==0.24.17
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -500,7 +500,7 @@ hole==0.5.1
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211103.0 home-assistant-frontend==20211108.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -689,7 +689,7 @@ p1monitor==1.0.0
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
paho-mqtt==1.5.1 paho-mqtt==1.6.1
# homeassistant.components.panasonic_viera # homeassistant.components.panasonic_viera
panasonic_viera==0.3.6 panasonic_viera==0.3.6
@ -1330,7 +1330,7 @@ tesla-powerwall==0.3.12
toonapi==0.2.1 toonapi==0.2.1
# homeassistant.components.totalconnect # homeassistant.components.totalconnect
total_connect_client==2021.8.3 total_connect_client==2021.11.2
# homeassistant.components.transmission # homeassistant.components.transmission
transmissionrpc==0.11 transmissionrpc==0.11
@ -1364,7 +1364,7 @@ url-normalize==1.4.1
uvcclient==0.11.0 uvcclient==0.11.0
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2021.11.0 velbus-aio==2021.11.6
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.14 venstarcolortouch==0.14

View File

@ -1,9 +1,7 @@
"""Common methods used across tests for TotalConnect.""" """Common methods used across tests for TotalConnect."""
from unittest.mock import patch from unittest.mock import patch
from total_connect_client.client import TotalConnectClient from total_connect_client import ArmingState, ResultCode, ZoneStatus, ZoneType
from total_connect_client.const import ArmingState
from total_connect_client.zone import ZoneStatus, ZoneType
from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -44,7 +42,7 @@ USER = {
} }
RESPONSE_AUTHENTICATE = { RESPONSE_AUTHENTICATE = {
"ResultCode": TotalConnectClient.SUCCESS, "ResultCode": ResultCode.SUCCESS.value,
"SessionID": 1, "SessionID": 1,
"Locations": LOCATIONS, "Locations": LOCATIONS,
"ModuleFlags": MODULE_FLAGS, "ModuleFlags": MODULE_FLAGS,
@ -52,7 +50,7 @@ RESPONSE_AUTHENTICATE = {
} }
RESPONSE_AUTHENTICATE_FAILED = { RESPONSE_AUTHENTICATE_FAILED = {
"ResultCode": TotalConnectClient.BAD_USER_OR_PASSWORD, "ResultCode": ResultCode.BAD_USER_OR_PASSWORD.value,
"ResultData": "test bad authentication", "ResultData": "test bad authentication",
} }
@ -255,18 +253,18 @@ RESPONSE_UNKNOWN = {
"ArmingState": ArmingState.DISARMED, "ArmingState": ArmingState.DISARMED,
} }
RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.ARM_SUCCESS} RESPONSE_ARM_SUCCESS = {"ResultCode": ResultCode.ARM_SUCCESS.value}
RESPONSE_ARM_FAILURE = {"ResultCode": TotalConnectClient.COMMAND_FAILED} RESPONSE_ARM_FAILURE = {"ResultCode": ResultCode.COMMAND_FAILED.value}
RESPONSE_DISARM_SUCCESS = {"ResultCode": TotalConnectClient.DISARM_SUCCESS} RESPONSE_DISARM_SUCCESS = {"ResultCode": ResultCode.DISARM_SUCCESS.value}
RESPONSE_DISARM_FAILURE = { RESPONSE_DISARM_FAILURE = {
"ResultCode": TotalConnectClient.COMMAND_FAILED, "ResultCode": ResultCode.COMMAND_FAILED.value,
"ResultData": "Command Failed", "ResultData": "Command Failed",
} }
RESPONSE_USER_CODE_INVALID = { RESPONSE_USER_CODE_INVALID = {
"ResultCode": TotalConnectClient.USER_CODE_INVALID, "ResultCode": ResultCode.USER_CODE_INVALID.value,
"ResultData": "testing user code invalid", "ResultData": "testing user code invalid",
} }
RESPONSE_SUCCESS = {"ResultCode": TotalConnectClient.SUCCESS} RESPONSE_SUCCESS = {"ResultCode": ResultCode.SUCCESS.value}
USERNAME = "username@me.com" USERNAME = "username@me.com"
PASSWORD = "password" PASSWORD = "password"
@ -292,7 +290,7 @@ PARTITION_DETAILS_2 = {
PARTITION_DETAILS = {"PartitionDetails": [PARTITION_DETAILS_1, PARTITION_DETAILS_2]} PARTITION_DETAILS = {"PartitionDetails": [PARTITION_DETAILS_1, PARTITION_DETAILS_2]}
RESPONSE_PARTITION_DETAILS = { RESPONSE_PARTITION_DETAILS = {
"ResultCode": TotalConnectClient.SUCCESS, "ResultCode": ResultCode.SUCCESS.value,
"ResultData": "testing partition details", "ResultData": "testing partition details",
"PartitionsInfoList": PARTITION_DETAILS, "PartitionsInfoList": PARTITION_DETAILS,
} }

View File

@ -95,7 +95,7 @@ async def test_abort_if_already_setup(hass):
with patch( with patch(
"homeassistant.components.totalconnect.config_flow.TotalConnectClient" "homeassistant.components.totalconnect.config_flow.TotalConnectClient"
) as client_mock: ) as client_mock:
client_mock.return_value.is_valid_credentials.return_value = True client_mock.return_value.is_logged_in.return_value = True
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},
@ -111,7 +111,7 @@ async def test_login_failed(hass):
with patch( with patch(
"homeassistant.components.totalconnect.config_flow.TotalConnectClient" "homeassistant.components.totalconnect.config_flow.TotalConnectClient"
) as client_mock: ) as client_mock:
client_mock.return_value.is_valid_credentials.return_value = False client_mock.return_value.is_logged_in.return_value = False
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},
@ -143,7 +143,7 @@ async def test_reauth(hass):
"homeassistant.components.totalconnect.async_setup_entry", return_value=True "homeassistant.components.totalconnect.async_setup_entry", return_value=True
): ):
# first test with an invalid password # first test with an invalid password
client_mock.return_value.is_valid_credentials.return_value = False client_mock.return_value.is_logged_in.return_value = False
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"} result["flow_id"], user_input={CONF_PASSWORD: "password"}
@ -153,7 +153,7 @@ async def test_reauth(hass):
assert result["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": "invalid_auth"}
# now test with the password valid # now test with the password valid
client_mock.return_value.is_valid_credentials.return_value = True client_mock.return_value.is_logged_in.return_value = True
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"} result["flow_id"], user_input={CONF_PASSWORD: "password"}

View File

@ -22,7 +22,7 @@ async def test_reauth_started(hass):
"homeassistant.components.totalconnect.TotalConnectClient", "homeassistant.components.totalconnect.TotalConnectClient",
autospec=True, autospec=True,
) as mock_client: ) as mock_client:
mock_client.return_value.is_valid_credentials.return_value = False mock_client.return_value.is_logged_in.return_value = False
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()