Merge pull request #63412 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-01-04 21:58:36 -08:00 committed by GitHub
commit cf84ba1da1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 422 additions and 145 deletions

View File

@ -248,8 +248,10 @@ class ConfiguredDoorBird:
if self.custom_url is not None:
hass_url = self.custom_url
favorites = self.device.favorites()
for event in self.doorstation_events:
self._register_event(hass_url, event)
self._register_event(hass_url, event, favs=favorites)
_LOGGER.info("Successfully registered URL for %s on %s", event, self.name)
@ -261,15 +263,15 @@ class ConfiguredDoorBird:
def _get_event_name(self, event):
return f"{self.slug}_{event}"
def _register_event(self, hass_url, event):
def _register_event(self, hass_url, event, favs=None):
"""Add a schedule entry in the device for a sensor."""
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
# Register HA URL as webhook if not already, then get the ID
if not self.webhook_is_registered(url):
if not self.webhook_is_registered(url, favs=favs):
self.device.change_favorite("http", f"Home Assistant ({event})", url)
if not self.get_webhook_id(url):
if not self.get_webhook_id(url, favs=favs):
_LOGGER.warning(
'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"',
url,

View File

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

View File

@ -160,7 +160,10 @@ class FroniusSolarNet:
)
if self.logger_coordinator:
_logger_info = self.logger_coordinator.data[SOLAR_NET_ID_SYSTEM]
solar_net_device[ATTR_MODEL] = _logger_info["product_type"]["value"]
# API v0 doesn't provide product_type
solar_net_device[ATTR_MODEL] = _logger_info.get("product_type", {}).get(
"value", "Datalogger Web"
)
solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][
"value"
]

View File

@ -2296,8 +2296,8 @@ class SensorStateTrait(_Trait):
sensor_types = {
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (

View File

@ -3,7 +3,7 @@
"name": "Gree Climate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gree",
"requirements": ["greeclimate==0.12.5"],
"requirements": ["greeclimate==1.0.1"],
"codeowners": ["@cmroche"],
"iot_class": "local_polling"
}

View File

@ -619,7 +619,9 @@ class GTFSDepartureSensor(SensorEntity):
if not self._departure:
self._state = None
else:
self._state = self._departure["departure_time"]
self._state = self._departure["departure_time"].replace(
tzinfo=dt_util.UTC
)
# Fetch trip and route details once, unless updated
if not self._departure:

View File

@ -134,7 +134,7 @@ class HassIOIngress(HomeAssistantView):
if (
hdrs.CONTENT_LENGTH in result.headers
and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000
):
) or result.status in (204, 304):
# Return Response
body = await result.read()
return web.Response(

View File

@ -17,13 +17,15 @@ from homeassistant.components import ssdp, zeroconf
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import aiohttp_client, device_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
CONF_API_VERSION,
CONF_IGNORE_AVAILABILITY,
DEFAULT_ALLOW_HUE_GROUPS,
DEFAULT_ALLOW_UNREACHABLE,
DOMAIN,
@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> HueOptionsFlowHandler:
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
"""Get the options flow for this handler."""
return HueOptionsFlowHandler(config_entry)
@classmethod
@callback
def async_supports_options_flow(
cls, config_entry: config_entries.ConfigEntry
) -> bool:
"""Return options flow support for this handler."""
return config_entry.data.get(CONF_API_VERSION, 1) == 1
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
return HueV1OptionsFlowHandler(config_entry)
return HueV2OptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the Hue flow."""
@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_link()
class HueOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Hue options."""
class HueV1OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Hue options for V1 implementation."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize Hue options flow."""
@ -319,3 +315,47 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow):
}
),
)
class HueV2OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Hue options for V2 implementation."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize Hue options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult:
"""Manage Hue options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
# create a list of Hue device ID's that the user can select
# to ignore availability status
dev_reg = device_registry.async_get(self.hass)
entries = device_registry.async_entries_for_config_entry(
dev_reg, self.config_entry.entry_id
)
dev_ids = {
identifier[1]: entry.name
for entry in entries
for identifier in entry.identifiers
if identifier[0] == DOMAIN
}
# filter any non existing device id's from the list
cur_ids = [
item
for item in self.config_entry.options.get(CONF_IGNORE_AVAILABILITY, [])
if item in dev_ids
]
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_IGNORE_AVAILABILITY,
default=cur_ids,
): cv.multi_select(dev_ids),
}
),
)

View File

@ -3,6 +3,7 @@
DOMAIN = "hue"
CONF_API_VERSION = "api_version"
CONF_IGNORE_AVAILABILITY = "ignore_availability"
CONF_SUBTYPE = "subtype"

View File

@ -70,7 +70,8 @@
"data": {
"allow_hue_groups": "Allow Hue groups",
"allow_hue_scenes": "Allow Hue scenes",
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
"ignore_availability": "Ignore connectivity status for the given devices"
}
}
}

View File

@ -69,7 +69,8 @@
"data": {
"allow_hue_groups": "Allow Hue groups",
"allow_hue_scenes": "Allow Hue scenes",
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
"ignore_availability": "Ignore connectivity status for the given devices"
}
}
}

View File

@ -69,7 +69,8 @@
"data": {
"allow_hue_groups": "Sta Hue-groepen toe",
"allow_hue_scenes": "Sta Hue sc\u00e8nes toe",
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden"
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden",
"ignore_availability": "Negeer beschikbaarheid status voor deze apparaten"
}
}
}

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from ..bridge import HueBridge
from ..const import DOMAIN
from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN
RESOURCE_TYPE_NAMES = {
# a simple mapping of hue resource type to Hass name
@ -71,7 +71,7 @@ class HueBaseEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
self._check_availability_workaround()
self._check_availability()
# Add value_changed callbacks.
self.async_on_remove(
self.controller.subscribe(
@ -80,7 +80,7 @@ class HueBaseEntity(Entity):
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
)
)
# also subscribe to device update event to catch devicer changes (e.g. name)
# also subscribe to device update event to catch device changes (e.g. name)
if self.device is None:
return
self.async_on_remove(
@ -92,25 +92,27 @@ class HueBaseEntity(Entity):
)
# subscribe to zigbee_connectivity to catch availability changes
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
self.bridge.api.sensors.zigbee_connectivity.subscribe(
self._handle_event,
zigbee.id,
EventType.RESOURCE_UPDATED,
self.async_on_remove(
self.bridge.api.sensors.zigbee_connectivity.subscribe(
self._handle_event,
zigbee.id,
EventType.RESOURCE_UPDATED,
)
)
@property
def available(self) -> bool:
"""Return entity availability."""
# entities without a device attached should be always available
if self.device is None:
# entities without a device attached should be always available
return True
# the zigbee connectivity sensor itself should be always available
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
# the zigbee connectivity sensor itself should be always available
return True
if self._ignore_availability:
return True
# all device-attached entities get availability from the zigbee connectivity
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity
return zigbee.status == ConnectivityServiceStatus.CONNECTED
return True
@ -130,30 +132,41 @@ class HueBaseEntity(Entity):
ent_reg.async_remove(self.entity_id)
else:
self.logger.debug("Received status update for %s", self.entity_id)
self._check_availability_workaround()
self._check_availability()
self.on_update()
self.async_write_ha_state()
@callback
def _check_availability_workaround(self):
def _check_availability(self):
"""Check availability of the device."""
if self.resource.type != ResourceTypes.LIGHT:
return
# return if we already processed this entity
if self._ignore_availability is not None:
# already processed
return
# only do the availability check for entities connected to a device
if self.device is None:
return
# ignore availability if user added device to ignore list
if self.device.id in self.bridge.config_entry.options.get(
CONF_IGNORE_AVAILABILITY, []
):
self._ignore_availability = True
self.logger.info(
"Device %s is configured to ignore availability status. ",
self.name,
)
return
# certified products (normally) report their state correctly
# no need for workaround/reporting
if self.device.product_data.certified:
# certified products report their state correctly
self._ignore_availability = False
return
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable
# it can be controlled. If the light is reported unavailable
# by the zigbee connectivity but the state changes its considered as a
# malfunctioning device and we report it.
# while the user should actually fix this issue instead of ignoring it, we
# ignore the availability for this light from this point.
# While the user should actually fix this issue, we allow to
# ignore the availability for this light/device from the config options.
cur_state = self.resource.on.on
if self._last_state is None:
self._last_state = cur_state
@ -166,9 +179,10 @@ class HueBaseEntity(Entity):
# the device state changed from on->off or off->on
# while it was reported as not connected!
self.logger.warning(
"Light %s changed state while reported as disconnected. "
"This might be an indicator that routing is not working for this device. "
"Home Assistant will ignore availability for this light from now on. "
"Device %s changed state while reported as disconnected. "
"This might be an indicator that routing is not working for this device "
"or the device is having connectivity issues. "
"You can disable availability reporting for this device in the Hue options. "
"Device details: %s - %s (%s) fw: %s",
self.name,
self.device.product_data.manufacturer_name,
@ -178,6 +192,4 @@ class HueBaseEntity(Entity):
)
# do we want to store this in some persistent storage?
self._ignore_availability = True
else:
self._ignore_availability = False
self._last_state = cur_state

View File

@ -7,7 +7,6 @@ from typing import Any
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from aiohue.v2.models.feature import AlertEffectType
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@ -103,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# Entities for Hue groups are disabled by default
# unless they were enabled in old version (legacy option)
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
self._attr_entity_registry_enabled_default = bridge.config_entry.options.get(
CONF_ALLOW_HUE_GROUPS, False
)
@ -193,7 +192,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
@ -300,7 +298,10 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
supported_color_modes.add(COLOR_MODE_ONOFF)
self._attr_supported_color_modes = supported_color_modes
# pick a winner for the current colormode
if lights_in_colortemp_mode == lights_with_color_temp_support:
if (
lights_with_color_temp_support > 0
and lights_in_colortemp_mode == lights_with_color_temp_support
):
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
elif lights_with_color_support > 0:
self._attr_color_mode = COLOR_MODE_XY

View File

@ -91,6 +91,9 @@ class HueLight(HueBaseEntity, LightEntity):
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
# support transition if brightness control
self._attr_supported_features |= SUPPORT_TRANSITION
self._last_xy: tuple[float, float] | None = self.xy_color
self._last_color_temp: int = self.color_temp
self._set_color_mode()
@property
def brightness(self) -> int | None:
@ -100,18 +103,6 @@ class HueLight(HueBaseEntity, LightEntity):
return round((dimming.brightness / 100) * 255)
return None
@property
def color_mode(self) -> str:
"""Return the current color mode of the light."""
if color_temp := self.resource.color_temperature:
if color_temp.mirek_valid and color_temp.mirek is not None:
return COLOR_MODE_COLOR_TEMP
if self.resource.supports_color:
return COLOR_MODE_XY
if self.resource.supports_dimming:
return COLOR_MODE_BRIGHTNESS
return COLOR_MODE_ONOFF
@property
def is_on(self) -> bool:
"""Return true if device is on (brightness above 0)."""
@ -158,6 +149,11 @@ class HueLight(HueBaseEntity, LightEntity):
"dynamics": self.resource.dynamics.status.value,
}
@callback
def on_update(self) -> None:
"""Call on update event."""
self._set_color_mode()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
@ -212,3 +208,43 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id,
short=flash == FLASH_SHORT,
)
@callback
def _set_color_mode(self) -> None:
"""Set current colormode of light."""
last_xy = self._last_xy
last_color_temp = self._last_color_temp
self._last_xy = self.xy_color
self._last_color_temp = self.color_temp
# Certified Hue lights return `mired_valid` to indicate CT is active
if color_temp := self.resource.color_temperature:
if color_temp.mirek_valid and color_temp.mirek is not None:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
return
# Non-certified lights do not report their current color mode correctly
# so we keep track of the color values to determine which is active
if last_color_temp != self.color_temp:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
return
if last_xy != self.xy_color:
self._attr_color_mode = COLOR_MODE_XY
return
# if we didn't detect any changes, abort and use previous values
if self._attr_color_mode is not None:
return
# color mode not yet determined, work it out here
# Note that for lights that do not correctly report `mirek_valid`
# we might have an invalid startup state which will be auto corrected
if self.resource.supports_color:
self._attr_color_mode = COLOR_MODE_XY
elif self.resource.supports_color_temperature:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
elif self.resource.supports_dimming:
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
else:
# fallback to on_off
self._attr_color_mode = COLOR_MODE_ONOFF

View File

@ -511,11 +511,6 @@ class ZoneDevice(ClimateEntity):
"""Return True if entity is available."""
return self._controller.available
@property
def assumed_state(self) -> bool:
"""Return True if unable to access real state of the entity."""
return self._controller.assumed_state
@property
def unique_id(self):
"""Return the ID of the controller device."""

View File

@ -2,11 +2,11 @@
"domain": "izone",
"name": "iZone",
"documentation": "https://www.home-assistant.io/integrations/izone",
"requirements": ["python-izone==1.1.8"],
"requirements": ["python-izone==1.2.3"],
"codeowners": ["@Swamp-Ig"],
"config_flow": true,
"homekit": {
"models": ["iZone"]
},
"iot_class": "local_push"
"iot_class": "local_polling"
}

View File

@ -28,6 +28,7 @@ from .schema import ConnectionSchema
CONF_KNX_GATEWAY: Final = "gateway"
CONF_MAX_RATE_LIMIT: Final = 60
CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0"
DEFAULT_ENTRY_DATA: Final = {
ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
@ -328,6 +329,12 @@ class KNXOptionsFlowHandler(OptionsFlow):
entry_data = {
**DEFAULT_ENTRY_DATA,
**self.general_settings,
ConnectionSchema.CONF_KNX_LOCAL_IP: self.general_settings.get(
ConnectionSchema.CONF_KNX_LOCAL_IP
)
if self.general_settings.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
!= CONF_DEFAULT_LOCAL_IP
else None,
CONF_HOST: self.current_config.get(CONF_HOST, ""),
}
@ -337,7 +344,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
**user_input,
}
entry_title = entry_data[CONF_KNX_CONNECTION_TYPE].capitalize()
entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize()
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
@ -388,12 +395,16 @@ class KNXOptionsFlowHandler(OptionsFlow):
}
if self.show_advanced_options:
local_ip = (
self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
if self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
is not None
else CONF_DEFAULT_LOCAL_IP
)
data_schema[
vol.Optional(
vol.Required(
ConnectionSchema.CONF_KNX_LOCAL_IP,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_LOCAL_IP,
),
default=local_ip,
)
] = str
data_schema[

View File

@ -20,7 +20,7 @@
"host": "[%key:common::config_flow::data::host%]",
"individual_address": "Individual address for the connection",
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
}
},
"routing": {
@ -29,7 +29,7 @@
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
}
}
},
@ -49,7 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}

View File

@ -12,7 +12,7 @@
"data": {
"host": "Host",
"individual_address": "Individual address for the connection",
"local_ip": "Local IP (leave empty if unsure)",
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
},
@ -21,9 +21,9 @@
"routing": {
"data": {
"individual_address": "Individual address for the routing connection",
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
"multicast_port": "The multicast port used for routing"
},
"description": "Please configure the routing options."
},
@ -47,9 +47,9 @@
"data": {
"connection_type": "KNX Connection Type",
"individual_address": "Default individual address",
"local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"rate_limit": "Maximum outgoing telegrams per second",
"state_updater": "Globally enable reading states from the KNX Bus"
}
@ -63,4 +63,4 @@
}
}
}
}
}

View File

@ -136,14 +136,13 @@ async def async_setup_entry(
for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
try:
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
except KeyError:
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
if (climate_state := data_handler.data[signal_name]) is None:
continue
climate_state = data_handler.data[signal_name]
climate_topology.register_handler(home_id, climate_state.process_topology)
for room in climate_state.homes[home_id].rooms.values():

View File

@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
"pyatmo==6.2.0"
"pyatmo==6.2.2"
],
"after_dependencies": [
"cloud",

View File

@ -49,17 +49,13 @@ async def async_setup_entry(
for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
try:
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
except KeyError:
continue
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
climate_state = data_handler.data.get(signal_name)
if (climate_state := data_handler.data[signal_name]) is None:
continue
climate_topology.register_handler(home_id, climate_state.process_topology)
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[

View File

@ -2,8 +2,10 @@
from __future__ import annotations
from ipaddress import IPv4Address, IPv6Address, ip_interface
import logging
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
@ -12,6 +14,8 @@ from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
from .models import Adapter
from .network import Network, async_get_network
_LOGGER = logging.getLogger(__name__)
@bind_hass
async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
@ -32,6 +36,16 @@ async def async_get_source_ip(
all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
source_ip = util.async_get_source_ip(target_ip)
if not all_ipv4s:
_LOGGER.warning(
"Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
)
if source_ip is None:
raise HomeAssistantError(
"Could not determine source ip because the system does not have any enabled IPv4 addresses and creating a socket failed"
)
return source_ip
return source_ip if source_ip in all_ipv4s else all_ipv4s[0]

View File

@ -35,7 +35,7 @@ NO_IP_ERRORS = {
"911": "A fatal error on NO-IP's side such as a database outage",
}
UPDATE_URL = "https://dynupdate.noip.com/nic/update"
UPDATE_URL = "https://dynupdate.no-ip.com/nic/update"
HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}"
CONFIG_SCHEMA = vol.Schema(

View File

@ -32,6 +32,8 @@ from .const import (
UNDO_UPDATE_LISTENER,
)
NUT_FAKE_SERIAL = ["unknown", "blank"]
_LOGGER = logging.getLogger(__name__)
@ -140,7 +142,9 @@ def _firmware_from_status(status):
def _serial_from_status(status):
"""Find the best serialvalue from the status."""
serial = status.get("device.serial") or status.get("ups.serial")
if serial and (serial.lower() == "unknown" or serial.count("0") == len(serial)):
if serial and (
serial.lower() in NUT_FAKE_SERIAL or serial.count("0") == len(serial)
):
return None
return serial

View File

@ -380,6 +380,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
for user in plex_server.option_monitored_users
if plex_server.option_monitored_users[user]["enabled"]
}
default_accounts.intersection_update(plex_server.accounts)
for user in plex_server.accounts:
if user not in known_accounts:
available_accounts[user] += " [New]"

View File

@ -219,7 +219,7 @@ class BlockSleepingClimate(
return CURRENT_HVAC_OFF
return (
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
CURRENT_HVAC_HEAT if bool(self.device_block.status) else CURRENT_HVAC_IDLE
)
@property

View File

@ -123,6 +123,9 @@ async def async_get_triggers(
append_input_triggers(triggers, input_triggers, device_id)
return triggers
if not block_wrapper.device.initialized:
return triggers
assert block_wrapper.device.blocks
for block in block_wrapper.device.blocks:

View File

@ -163,7 +163,7 @@ class SisyphusPlayer(MediaPlayerEntity):
if self._table.active_track:
return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE)
return super.media_image_url()
return super().media_image_url
async def async_turn_on(self):
"""Wake up a sleeping table."""

View File

@ -307,6 +307,7 @@ CPU_SENSOR_PREFIXES = [
"soc_thermal 1",
"Tctl",
"cpu0-thermal",
"cpu0_thermal",
]

View File

@ -112,9 +112,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
self._supported_features |= SUPPORT_FAN_SPEED
self._fan_speed_type = EnumTypeData.from_json(function.values)
if function := device.function.get(DPCode.ELECTRICITY_LEFT):
if status_range := device.status_range.get(DPCode.ELECTRICITY_LEFT):
self._supported_features |= SUPPORT_BATTERY
self._battery_level_type = IntegerTypeData.from_json(function.values)
self._battery_level_type = IntegerTypeData.from_json(status_range.values)
@property
def battery_level(self) -> int | None:

View File

@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.9.2"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES
from homeassistant.core import callback
@ -248,6 +249,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
return
for feature, description in NUMBER_TYPES.items():
if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5:
# Delete LED bightness entity created by mistake if it exists
entity_reg = hass.helpers.entity_registry.async_get()
entity_id = entity_reg.async_get_entity_id(
PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}"
)
if entity_id:
entity_reg.async_remove(entity_id)
continue
if feature & features:
if (
description.key == ATTR_OSCILLATION_ANGLE

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "7"
PATCH_VERSION: Final = "8"
__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

@ -659,7 +659,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.21
flux_led==0.27.32
# homeassistant.components.homekit
fnvhash==0.1.0
@ -754,7 +754,7 @@ gpiozero==1.5.1
gps3==0.33.3
# homeassistant.components.gree
greeclimate==0.12.5
greeclimate==1.0.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==2.1
@ -1006,7 +1006,7 @@ meteofrance-api==1.0.2
mficlient==0.3.0
# homeassistant.components.xiaomi_miio
micloud==0.4
micloud==0.5
# homeassistant.components.miflora
miflora==0.7.0
@ -1364,7 +1364,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==6.2.0
pyatmo==6.2.2
# homeassistant.components.atome
pyatome==0.1.1
@ -1890,7 +1890,7 @@ python-gitlab==1.6.0
python-hpilo==4.3
# homeassistant.components.izone
python-izone==1.1.8
python-izone==1.2.3
# homeassistant.components.joaoapps_join
python-join-api==0.0.6

View File

@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.21
flux_led==0.27.32
# homeassistant.components.homekit
fnvhash==0.1.0
@ -467,7 +467,7 @@ google-nest-sdm==0.4.9
googlemaps==2.5.1
# homeassistant.components.gree
greeclimate==0.12.5
greeclimate==1.0.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==2.1
@ -612,7 +612,7 @@ meteofrance-api==1.0.2
mficlient==0.3.0
# homeassistant.components.xiaomi_miio
micloud==0.4
micloud==0.5
# homeassistant.components.mill
mill-local==0.1.0
@ -832,7 +832,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==6.2.0
pyatmo==6.2.2
# homeassistant.components.apple_tv
pyatv==0.8.2
@ -1136,7 +1136,7 @@ python-ecobee-api==0.2.14
python-forecastio==1.4.0
# homeassistant.components.izone
python-izone==1.1.8
python-izone==1.2.3
# homeassistant.components.juicenet
python-juicenet==1.0.2

View File

@ -3030,8 +3030,8 @@ async def test_sensorstate(hass):
"""Test SensorState trait support for sensor domain."""
sensor_types = {
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (

View File

@ -11,6 +11,7 @@ from homeassistant import config_entries
from homeassistant.components import ssdp, zeroconf
from homeassistant.components.hue import config_flow, const
from homeassistant.components.hue.errors import CannotConnect
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@ -701,12 +702,33 @@ async def test_options_flow_v2(hass):
"""Test options config flow for a V2 bridge."""
entry = MockConfigEntry(
domain="hue",
unique_id="v2bridge",
unique_id="aabbccddeeff",
data={"host": "0.0.0.0", "api_version": 2},
)
entry.add_to_hass(hass)
assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False
dev_reg = dr.async_get(hass)
mock_dev_id = "aabbccddee"
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id, identifiers={(const.DOMAIN, mock_dev_id)}
)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
schema = result["data_schema"].schema
assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == []
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]},
)
assert result["type"] == "create_entry"
assert result["data"] == {
const.CONF_IGNORE_AVAILABILITY: [mock_dev_id],
}
async def test_bridge_zeroconf(hass, aioclient_mock):

View File

@ -1,6 +1,7 @@
"""Test the KNX config flow."""
from unittest.mock import patch
import pytest
from xknx import XKNX
from xknx.io import DEFAULT_MCAST_GRP
from xknx.io.gateway_scanner import GatewayDescriptor
@ -8,6 +9,7 @@ from xknx.io.gateway_scanner import GatewayDescriptor
from homeassistant import config_entries
from homeassistant.components.knx import ConnectionSchema
from homeassistant.components.knx.config_flow import (
CONF_DEFAULT_LOCAL_IP,
CONF_KNX_GATEWAY,
DEFAULT_ENTRY_DATA,
)
@ -585,6 +587,7 @@ async def test_options_flow(
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
@ -643,14 +646,65 @@ async def test_tunneling_options_flow(
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
}
@pytest.mark.parametrize(
"user_input,config_entry_data",
[
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP,
},
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
},
),
],
)
async def test_advanced_options(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
user_input,
config_entry_data,
) -> None:
"""Test options config flow."""
mock_config_entry.add_to_hass(hass)
@ -668,28 +722,11 @@ async def test_advanced_options(
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
user_input=user_input,
)
await hass.async_block_till_done()
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert not result2.get("data")
assert mock_config_entry.data == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
}
assert mock_config_entry.data == config_entry_data

View File

@ -3,6 +3,7 @@ from ipaddress import IPv4Address
from unittest.mock import MagicMock, Mock, patch
import ifaddr
import pytest
from homeassistant.components import network
from homeassistant.components.network.const import (
@ -13,6 +14,7 @@ from homeassistant.components.network.const import (
STORAGE_KEY,
STORAGE_VERSION,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
_NO_LOOPBACK_IPADDR = "192.168.1.5"
@ -602,3 +604,49 @@ async def test_async_get_ipv4_broadcast_addresses_multiple(hass, hass_storage):
IPv4Address("192.168.1.255"),
IPv4Address("169.254.255.255"),
}
async def test_async_get_source_ip_no_enabled_addresses(hass, hass_storage, caplog):
"""Test getting the source ip address when all adapters are disabled."""
hass_storage[STORAGE_KEY] = {
"version": STORAGE_VERSION,
"key": STORAGE_KEY,
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]},
}
with patch(
"homeassistant.components.network.util.ifaddr.get_adapters",
return_value=[],
), patch(
"homeassistant.components.network.util.socket.socket",
return_value=_mock_socket(["192.168.1.5"]),
):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5"
assert "source address detection may be inaccurate" in caplog.text
async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses(
hass, hass_storage, caplog
):
"""Test getting the source ip address when all adapters are disabled and getting it fails."""
hass_storage[STORAGE_KEY] = {
"version": STORAGE_VERSION,
"key": STORAGE_KEY,
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]},
}
with patch(
"homeassistant.components.network.util.ifaddr.get_adapters",
return_value=[],
), patch(
"homeassistant.components.network.util.socket.socket",
return_value=_mock_socket([None]),
):
assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError):
await network.async_get_source_ip(hass, MDNS_TARGET_IP)

View File

@ -168,6 +168,42 @@ async def test_get_triggers_button(hass):
assert_lists_same(triggers, expected_triggers)
async def test_get_triggers_non_initialized_devices(hass):
"""Test we get the empty triggers for non-initialized devices."""
await async_setup_component(hass, "shelly", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={"sleep_period": 43200, "model": "SHDW-2", "host": "1.2.3.4"},
unique_id="12345678",
)
config_entry.add_to_hass(hass)
device = Mock(
blocks=None,
settings=None,
shelly=None,
update=AsyncMock(),
initialized=False,
)
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {}
coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
BLOCK
] = BlockDeviceWrapper(hass, config_entry, device)
coap_wrapper.async_setup()
expected_triggers = []
triggers = await async_get_device_automations(
hass, "trigger", coap_wrapper.device_id
)
assert_lists_same(triggers, expected_triggers)
async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper):
"""Test error raised for invalid shelly device_id."""
assert coap_wrapper