Merge pull request #73334 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-06-10 14:01:57 -07:00 committed by GitHub
commit 3759adcf2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 218 additions and 125 deletions

View File

@ -70,6 +70,7 @@ class FeedManager:
self._last_entry_timestamp = None self._last_entry_timestamp = None
self._last_update_successful = False self._last_update_successful = False
self._has_published_parsed = False self._has_published_parsed = False
self._has_updated_parsed = False
self._event_type = EVENT_FEEDREADER self._event_type = EVENT_FEEDREADER
self._feed_id = url self._feed_id = url
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update())
@ -122,7 +123,7 @@ class FeedManager:
) )
self._filter_entries() self._filter_entries()
self._publish_new_entries() self._publish_new_entries()
if self._has_published_parsed: if self._has_published_parsed or self._has_updated_parsed:
self._storage.put_timestamp( self._storage.put_timestamp(
self._feed_id, self._last_entry_timestamp self._feed_id, self._last_entry_timestamp
) )
@ -143,7 +144,7 @@ class FeedManager:
def _update_and_fire_entry(self, entry): def _update_and_fire_entry(self, entry):
"""Update last_entry_timestamp and fire entry.""" """Update last_entry_timestamp and fire entry."""
# Check if the entry has a published date. # Check if the entry has a published or updated date.
if "published_parsed" in entry and entry.published_parsed: if "published_parsed" in entry and entry.published_parsed:
# We are lucky, `published_parsed` data available, let's make use of # We are lucky, `published_parsed` data available, let's make use of
# it to publish only new available entries since the last run # it to publish only new available entries since the last run
@ -151,9 +152,20 @@ class FeedManager:
self._last_entry_timestamp = max( self._last_entry_timestamp = max(
entry.published_parsed, self._last_entry_timestamp entry.published_parsed, self._last_entry_timestamp
) )
elif "updated_parsed" in entry and entry.updated_parsed:
# We are lucky, `updated_parsed` data available, let's make use of
# it to publish only new available entries since the last run
self._has_updated_parsed = True
self._last_entry_timestamp = max(
entry.updated_parsed, self._last_entry_timestamp
)
else: else:
self._has_published_parsed = False self._has_published_parsed = False
_LOGGER.debug("No published_parsed info available for entry %s", entry) self._has_updated_parsed = False
_LOGGER.debug(
"No published_parsed or updated_parsed info available for entry %s",
entry,
)
entry.update({"feed_url": self._url}) entry.update({"feed_url": self._url})
self._hass.bus.fire(self._event_type, entry) self._hass.bus.fire(self._event_type, entry)
@ -167,9 +179,16 @@ class FeedManager:
# Set last entry timestamp as epoch time if not available # Set last entry timestamp as epoch time if not available
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
for entry in self._feed.entries: for entry in self._feed.entries:
if self._firstrun or ( if (
"published_parsed" in entry self._firstrun
and entry.published_parsed > self._last_entry_timestamp or (
"published_parsed" in entry
and entry.published_parsed > self._last_entry_timestamp
)
or (
"updated_parsed" in entry
and entry.updated_parsed > self._last_entry_timestamp
)
): ):
self._update_and_fire_entry(entry) self._update_and_fire_entry(entry)
new_entries = True new_entries = True

View File

@ -460,7 +460,7 @@ async def _async_setup_themes(
async def reload_themes(_: ServiceCall) -> None: async def reload_themes(_: ServiceCall) -> None:
"""Reload themes.""" """Reload themes."""
config = await async_hass_config_yaml(hass) config = await async_hass_config_yaml(hass)
new_themes = config[DOMAIN].get(CONF_THEMES, {}) new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {})
hass.data[DATA_THEMES] = new_themes hass.data[DATA_THEMES] = new_themes
if hass.data[DATA_DEFAULT_THEME] not in new_themes: if hass.data[DATA_DEFAULT_THEME] not in new_themes:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME

View File

@ -27,6 +27,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.data = {} self.data = {}
self.tokens = {} self.tokens = {}
self.entry = None self.entry = None
self.device_registration = False
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Prompt user input. Create or edit entry.""" """Prompt user input. Create or edit entry."""
@ -88,6 +89,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not errors: if not errors:
try: try:
self.device_registration = True
return await self.async_setup_hive_entry() return await self.async_setup_hive_entry()
except UnknownHiveError: except UnknownHiveError:
errors["base"] = "unknown" errors["base"] = "unknown"
@ -102,9 +104,10 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
raise UnknownHiveError raise UnknownHiveError
# Setup the config entry # Setup the config entry
await self.hive_auth.device_registration("Home Assistant") if self.device_registration:
await self.hive_auth.device_registration("Home Assistant")
self.data["device_data"] = await self.hive_auth.getDeviceData()
self.data["tokens"] = self.tokens self.data["tokens"] = self.tokens
self.data["device_data"] = await self.hive_auth.getDeviceData()
if self.context["source"] == config_entries.SOURCE_REAUTH: if self.context["source"] == config_entries.SOURCE_REAUTH:
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
self.entry, title=self.data["username"], data=self.data self.entry, title=self.data["username"], data=self.data

View File

@ -3,7 +3,7 @@
"name": "Hive", "name": "Hive",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hive", "documentation": "https://www.home-assistant.io/integrations/hive",
"requirements": ["pyhiveapi==0.5.5"], "requirements": ["pyhiveapi==0.5.9"],
"codeowners": ["@Rendili", "@KJonline"], "codeowners": ["@Rendili", "@KJonline"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["apyhiveapi"] "loggers": ["apyhiveapi"]

View File

@ -28,7 +28,14 @@ from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers import config_validation as cv, event, template
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.reload import (
async_integration_yaml_config,
async_setup_reload_service,
)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
@ -60,12 +67,14 @@ from .const import ( # noqa: F401
DATA_MQTT, DATA_MQTT,
DATA_MQTT_CONFIG, DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_RELOAD_NEEDED,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING, DEFAULT_ENCODING,
DEFAULT_QOS, DEFAULT_QOS,
DEFAULT_RETAIN, DEFAULT_RETAIN,
DOMAIN, DOMAIN,
MQTT_CONNECTED, MQTT_CONNECTED,
MQTT_DISCONNECTED, MQTT_DISCONNECTED,
MQTT_RELOADED,
PLATFORMS, PLATFORMS,
) )
from .models import ( # noqa: F401 from .models import ( # noqa: F401
@ -227,7 +236,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
await _async_setup_discovery(hass, mqtt_client.conf, entry) await _async_setup_discovery(hass, mqtt_client.conf, entry)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Load a config entry.""" """Load a config entry."""
# Merge basic configuration, and add missing defaults for basic options # Merge basic configuration, and add missing defaults for basic options
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
@ -364,6 +375,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set() hass.data[CONFIG_ENTRY_IS_SETUP] = set()
# Setup reload service. Once support for legacy config is removed in 2022.9, we
# should no longer call async_setup_reload_service but instead implement a custom
# service
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async def _async_reload_platforms(_: Event | None) -> None:
"""Discover entities for a platform."""
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {})
async_dispatcher_send(hass, MQTT_RELOADED)
async def async_forward_entry_setup(): async def async_forward_entry_setup():
"""Forward the config entry setup to the platforms.""" """Forward the config entry setup to the platforms."""
async with hass.data[DATA_CONFIG_ENTRY_LOCK]: async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
@ -374,6 +396,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
entry, component entry, component
) )
# Setup reload service after all platforms have loaded
entry.async_on_unload(
hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms)
)
hass.async_create_task(async_forward_entry_setup()) hass.async_create_task(async_forward_entry_setup())

View File

@ -35,6 +35,7 @@ DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock"
DATA_MQTT = "mqtt" DATA_MQTT = "mqtt"
DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_CONFIG = "mqtt_config"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"
DEFAULT_PREFIX = "homeassistant" DEFAULT_PREFIX = "homeassistant"
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
@ -63,6 +64,7 @@ DOMAIN = "mqtt"
MQTT_CONNECTED = "mqtt_connected" MQTT_CONNECTED = "mqtt_connected"
MQTT_DISCONNECTED = "mqtt_disconnected" MQTT_DISCONNECTED = "mqtt_disconnected"
MQTT_RELOADED = "mqtt_reloaded"
PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None" PAYLOAD_NONE = "None"

View File

@ -25,7 +25,6 @@ from homeassistant.const import (
STATE_CLOSING, STATE_CLOSING,
STATE_OPEN, STATE_OPEN,
STATE_OPENING, STATE_OPENING,
STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -470,7 +469,6 @@ class MqttCover(MqttEntity, CoverEntity):
} }
if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: if self._config.get(CONF_TILT_STATUS_TOPIC) is not None:
self._tilt_value = STATE_UNKNOWN
topics["tilt_status_topic"] = { topics["tilt_status_topic"] = {
"topic": self._config.get(CONF_TILT_STATUS_TOPIC), "topic": self._config.get(CONF_TILT_STATUS_TOPIC),
"msg_callback": tilt_message_received, "msg_callback": tilt_message_received,

View File

@ -48,10 +48,6 @@ from homeassistant.helpers.entity import (
async_generate_entity_id, async_generate_entity_id,
) )
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.reload import (
async_integration_yaml_config,
async_setup_reload_service,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import debug_info, subscription from . import debug_info, subscription
@ -67,13 +63,14 @@ from .const import (
DATA_MQTT, DATA_MQTT,
DATA_MQTT_CONFIG, DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_RELOAD_NEEDED,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING, DEFAULT_ENCODING,
DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_AVAILABLE,
DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE,
DOMAIN, DOMAIN,
MQTT_CONNECTED, MQTT_CONNECTED,
MQTT_DISCONNECTED, MQTT_DISCONNECTED,
PLATFORMS, MQTT_RELOADED,
) )
from .debug_info import log_message, log_messages from .debug_info import log_message, log_messages
from .discovery import ( from .discovery import (
@ -270,14 +267,11 @@ async def async_setup_platform_discovery(
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Set up platform discovery for manual config.""" """Set up platform discovery for manual config."""
async def _async_discover_entities(event: Event | None) -> None: async def _async_discover_entities() -> None:
"""Discover entities for a platform.""" """Discover entities for a platform."""
if event: if DATA_MQTT_UPDATED_CONFIG in hass.data:
# The platform has been reloaded # The platform has been reloaded
config_yaml = await async_integration_yaml_config(hass, DOMAIN) config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG]
if not config_yaml:
return
config_yaml = config_yaml.get(DOMAIN, {})
else: else:
config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) config_yaml = hass.data.get(DATA_MQTT_CONFIG, {})
if not config_yaml: if not config_yaml:
@ -293,8 +287,8 @@ async def async_setup_platform_discovery(
) )
) )
unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities)
await _async_discover_entities(None) await _async_discover_entities()
return unsub return unsub
@ -359,7 +353,6 @@ async def async_setup_platform_helper(
async_setup_entities: SetupEntity, async_setup_entities: SetupEntity,
) -> None: ) -> None:
"""Return true if platform setup should be aborted.""" """Return true if platform setup should be aborted."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
if not bool(hass.config_entries.async_entries(DOMAIN)): if not bool(hass.config_entries.async_entries(DOMAIN)):
hass.data[DATA_MQTT_RELOAD_NEEDED] = None hass.data[DATA_MQTT_RELOAD_NEEDED] = None
_LOGGER.warning( _LOGGER.warning(

View File

@ -18,7 +18,6 @@ from .const import (
KEY_COORDINATOR_SPEED, KEY_COORDINATOR_SPEED,
KEY_COORDINATOR_TRAFFIC, KEY_COORDINATOR_TRAFFIC,
KEY_ROUTER, KEY_ROUTER,
MODE_ROUTER,
PLATFORMS, PLATFORMS,
) )
from .errors import CannotLoginException from .errors import CannotLoginException
@ -72,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_update_devices() -> bool: async def async_update_devices() -> bool:
"""Fetch data from the router.""" """Fetch data from the router."""
if router.mode == MODE_ROUTER: if router.track_devices:
return await router.async_update_device_trackers() return await router.async_update_device_trackers()
return False return False
@ -107,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_interval=SPEED_TEST_INTERVAL, update_interval=SPEED_TEST_INTERVAL,
) )
if router.mode == MODE_ROUTER: if router.track_devices:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
await coordinator_traffic_meter.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh()
@ -134,7 +133,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not hass.data[DOMAIN]: if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
if router.mode != MODE_ROUTER: if not router.track_devices:
router_id = None router_id = None
# Remove devices that are no longer tracked # Remove devices that are no longer tracked
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)

View File

@ -80,6 +80,7 @@ class NetgearRouter:
self.hardware_version = "" self.hardware_version = ""
self.serial_number = "" self.serial_number = ""
self.track_devices = True
self.method_version = 1 self.method_version = 1
consider_home_int = entry.options.get( consider_home_int = entry.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
@ -112,11 +113,23 @@ class NetgearRouter:
self.serial_number = self._info["SerialNumber"] self.serial_number = self._info["SerialNumber"]
self.mode = self._info.get("DeviceMode", MODE_ROUTER) self.mode = self._info.get("DeviceMode", MODE_ROUTER)
enabled_entries = [
entry
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.disabled_by is None
]
self.track_devices = self.mode == MODE_ROUTER or len(enabled_entries) == 1
_LOGGER.debug(
"Netgear track_devices = '%s', device mode '%s'",
self.track_devices,
self.mode,
)
for model in MODELS_V2: for model in MODELS_V2:
if self.model.startswith(model): if self.model.startswith(model):
self.method_version = 2 self.method_version = 2
if self.method_version == 2 and self.mode == MODE_ROUTER: if self.method_version == 2 and self.track_devices:
if not self._api.get_attached_devices_2(): if not self._api.get_attached_devices_2():
_LOGGER.error( _LOGGER.error(
"Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2",
@ -133,7 +146,7 @@ class NetgearRouter:
return False return False
# set already known devices to away instead of unavailable # set already known devices to away instead of unavailable
if self.mode == MODE_ROUTER: if self.track_devices:
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) devices = dr.async_entries_for_config_entry(device_registry, self.entry_id)
for device_entry in devices: for device_entry in devices:

View File

@ -5,6 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
import logging
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
RestoreSensor, RestoreSensor,
@ -34,6 +35,8 @@ from .const import (
) )
from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { SENSOR_TYPES = {
"type": SensorEntityDescription( "type": SensorEntityDescription(
key="type", key="type",
@ -114,7 +117,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewWeekUpload", key="NewWeekUpload",
@ -123,7 +126,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewWeekDownload", key="NewWeekDownload",
@ -132,7 +135,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewWeekDownload", key="NewWeekDownload",
@ -141,7 +144,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewMonthUpload", key="NewMonthUpload",
@ -150,7 +153,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewMonthUpload", key="NewMonthUpload",
@ -159,7 +162,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewMonthDownload", key="NewMonthDownload",
@ -168,7 +171,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewMonthDownload", key="NewMonthDownload",
@ -177,7 +180,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewLastMonthUpload", key="NewLastMonthUpload",
@ -186,7 +189,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewLastMonthUpload", key="NewLastMonthUpload",
@ -195,7 +198,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:upload", icon="mdi:upload",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewLastMonthDownload", key="NewLastMonthDownload",
@ -204,7 +207,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=0, index=0,
value=lambda data: data[0] if data is not None else None, value=lambda data: data[0],
), ),
NetgearSensorEntityDescription( NetgearSensorEntityDescription(
key="NewLastMonthDownload", key="NewLastMonthDownload",
@ -213,7 +216,7 @@ SENSOR_TRAFFIC_TYPES = [
native_unit_of_measurement=DATA_MEGABYTES, native_unit_of_measurement=DATA_MEGABYTES,
icon="mdi:download", icon="mdi:download",
index=1, index=1,
value=lambda data: data[1] if data is not None else None, value=lambda data: data[1],
), ),
] ]
@ -372,6 +375,17 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor):
@callback @callback
def async_update_device(self) -> None: def async_update_device(self) -> None:
"""Update the Netgear device.""" """Update the Netgear device."""
if self.coordinator.data is not None: if self.coordinator.data is None:
data = self.coordinator.data.get(self.entity_description.key) return
self._value = self.entity_description.value(data)
data = self.coordinator.data.get(self.entity_description.key)
if data is None:
self._value = None
_LOGGER.debug(
"key '%s' not in Netgear router response '%s'",
self.entity_description.key,
data,
)
return
self._value = self.entity_description.value(data)

View File

@ -3,7 +3,7 @@
"name": "RainMachine", "name": "RainMachine",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine", "documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.06.0"], "requirements": ["regenmaschine==2022.06.1"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "local_polling", "iot_class": "local_polling",
"homekit": { "homekit": {

View File

@ -5,15 +5,18 @@ from sqlalchemy import text
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
def db_size_bytes(session: Session, database_name: str) -> float: def db_size_bytes(session: Session, database_name: str) -> float | None:
"""Get the mysql database size.""" """Get the mysql database size."""
return float( size = session.execute(
session.execute( text(
text( "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) "
"SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " "FROM information_schema.TABLES WHERE "
"FROM information_schema.TABLES WHERE " "TABLE_SCHEMA=:database_name"
"TABLE_SCHEMA=:database_name" ),
), {"database_name": database_name},
{"database_name": database_name}, ).first()[0]
).first()[0]
) if size is None:
return None
return float(size)

View File

@ -1,6 +1,7 @@
"""Support for balance data via the Starling Bank API.""" """Support for balance data via the Starling Bank API."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
import requests import requests
@ -26,6 +27,7 @@ DEFAULT_SANDBOX = False
DEFAULT_ACCOUNT_NAME = "Starling" DEFAULT_ACCOUNT_NAME = "Starling"
ICON = "mdi:currency-gbp" ICON = "mdi:currency-gbp"
SCAN_INTERVAL = timedelta(seconds=180)
ACCOUNT_SCHEMA = vol.Schema( ACCOUNT_SCHEMA = vol.Schema(
{ {

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from datetime import timedelta
import logging import logging
from synology_dsm import SynologyDSM from synology_dsm import SynologyDSM
@ -98,7 +97,7 @@ class SynoApi:
self._async_setup_api_requests() self._async_setup_api_requests()
await self._hass.async_add_executor_job(self._fetch_device_configuration) await self._hass.async_add_executor_job(self._fetch_device_configuration)
await self.async_update() await self.async_update(first_setup=True)
@callback @callback
def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]:
@ -251,7 +250,7 @@ class SynoApi:
# ignore API errors during logout # ignore API errors during logout
pass pass
async def async_update(self, now: timedelta | None = None) -> None: async def async_update(self, first_setup: bool = False) -> None:
"""Update function for updating API information.""" """Update function for updating API information."""
LOGGER.debug("Start data update for '%s'", self._entry.unique_id) LOGGER.debug("Start data update for '%s'", self._entry.unique_id)
self._async_setup_api_requests() self._async_setup_api_requests()
@ -259,14 +258,22 @@ class SynoApi:
await self._hass.async_add_executor_job( await self._hass.async_add_executor_job(
self.dsm.update, self._with_information self.dsm.update, self._with_information
) )
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: except (
LOGGER.warning( SynologyDSMLoginFailedException,
"Connection error during update, fallback by reloading the entry" SynologyDSMRequestException,
) SynologyDSMAPIErrorException,
) as err:
LOGGER.debug( LOGGER.debug(
"Connection error during update of '%s' with exception: %s", "Connection error during update of '%s' with exception: %s",
self._entry.unique_id, self._entry.unique_id,
err, err,
) )
if first_setup:
raise err
LOGGER.warning(
"Connection error during update, fallback by reloading the entry"
)
await self._hass.config_entries.async_reload(self._entry.entry_id) await self._hass.config_entries.async_reload(self._entry.entry_id)
return return

View File

@ -3,7 +3,7 @@
"name": "UniFi Protect", "name": "UniFi Protect",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect", "documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": ["pyunifiprotect==3.6.0", "unifi-discovery==1.1.3"], "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"],
"dependencies": ["http"], "dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@ -4,10 +4,6 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wallbox", "documentation": "https://www.home-assistant.io/integrations/wallbox",
"requirements": ["wallbox==0.4.9"], "requirements": ["wallbox==0.4.9"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@hesselonline"], "codeowners": ["@hesselonline"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["wallbox"] "loggers": ["wallbox"]

View File

@ -167,8 +167,12 @@ class WallboxSensor(WallboxEntity, SensorEntity):
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor. Round the value when it, and the precision property are not None."""
if (sensor_round := self.entity_description.precision) is not None: if (
sensor_round := self.entity_description.precision
) is not None and self.coordinator.data[
self.entity_description.key
] is not None:
return cast( return cast(
StateType, StateType,
round(self.coordinator.data[self.entity_description.key], sensor_round), round(self.coordinator.data[self.entity_description.key], sensor_round),

View File

@ -3,7 +3,7 @@
"name": "YoLink", "name": "YoLink",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/yolink", "documentation": "https://www.home-assistant.io/integrations/yolink",
"requirements": ["yolink-api==0.0.6"], "requirements": ["yolink-api==0.0.8"],
"dependencies": ["auth", "application_credentials"], "dependencies": ["auth", "application_credentials"],
"codeowners": ["@matrixd2"], "codeowners": ["@matrixd2"],
"iot_class": "cloud_push" "iot_class": "cloud_push"

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "4" PATCH_VERSION: Final = "5"
__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, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -1538,7 +1538,7 @@ pyheos==0.7.2
pyhik==0.3.0 pyhik==0.3.0
# homeassistant.components.hive # homeassistant.components.hive
pyhiveapi==0.5.5 pyhiveapi==0.5.9
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.77 pyhomematic==0.1.77
@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.6.0 pyunifiprotect==3.9.2
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -2065,7 +2065,7 @@ raincloudy==0.0.7
raspyrfm-client==1.2.8 raspyrfm-client==1.2.8
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2022.06.0 regenmaschine==2022.06.1
# homeassistant.components.renault # homeassistant.components.renault
renault-api==0.1.11 renault-api==0.1.11
@ -2486,7 +2486,7 @@ yeelight==0.7.10
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.0.6 yolink-api==0.0.8
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.16 youless-api==0.16

View File

@ -1029,7 +1029,7 @@ pyhaversion==22.4.1
pyheos==0.7.2 pyheos==0.7.2
# homeassistant.components.hive # homeassistant.components.hive
pyhiveapi==0.5.5 pyhiveapi==0.5.9
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.77 pyhomematic==0.1.77
@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.6.0 pyunifiprotect==3.9.2
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1364,7 +1364,7 @@ rachiopy==1.0.3
radios==0.1.1 radios==0.1.1
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2022.06.0 regenmaschine==2022.06.1
# homeassistant.components.renault # homeassistant.components.renault
renault-api==0.1.11 renault-api==0.1.11
@ -1638,7 +1638,7 @@ yalexs==1.1.25
yeelight==0.7.10 yeelight==0.7.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.0.6 yolink-api==0.0.8
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.16 youless-api==0.16

View File

@ -1,5 +1,5 @@
[metadata] [metadata]
version = 2022.6.4 version = 2022.6.5
url = https://www.home-assistant.io/ url = https://www.home-assistant.io/
[options] [options]

View File

@ -23,6 +23,7 @@ VALID_CONFIG_1 = {feedreader.DOMAIN: {CONF_URLS: [URL]}}
VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}} VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}}
VALID_CONFIG_3 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 100}} VALID_CONFIG_3 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 100}}
VALID_CONFIG_4 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}} VALID_CONFIG_4 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}}
VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}}
def load_fixture_bytes(src): def load_fixture_bytes(src):
@ -56,6 +57,12 @@ def fixture_feed_three_events(hass):
return load_fixture_bytes("feedreader3.xml") return load_fixture_bytes("feedreader3.xml")
@pytest.fixture(name="feed_atom_event")
def fixture_feed_atom_event(hass):
"""Load test feed data for atom event."""
return load_fixture_bytes("feedreader5.xml")
@pytest.fixture(name="events") @pytest.fixture(name="events")
async def fixture_events(hass): async def fixture_events(hass):
"""Fixture that catches alexa events.""" """Fixture that catches alexa events."""
@ -98,7 +105,7 @@ async def test_setup_max_entries(hass):
async def test_feed(hass, events, feed_one_event): async def test_feed(hass, events, feed_one_event):
"""Test simple feed with valid data.""" """Test simple rss feed with valid data."""
with patch( with patch(
"feedparser.http.get", "feedparser.http.get",
return_value=feed_one_event, return_value=feed_one_event,
@ -120,6 +127,29 @@ async def test_feed(hass, events, feed_one_event):
assert events[0].data.published_parsed.tm_min == 10 assert events[0].data.published_parsed.tm_min == 10
async def test_atom_feed(hass, events, feed_atom_event):
"""Test simple atom feed with valid data."""
with patch(
"feedparser.http.get",
return_value=feed_atom_event,
):
assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_5)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert len(events) == 1
assert events[0].data.title == "Atom-Powered Robots Run Amok"
assert events[0].data.description == "Some text."
assert events[0].data.link == "http://example.org/2003/12/13/atom03"
assert events[0].data.id == "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
assert events[0].data.updated_parsed.tm_year == 2003
assert events[0].data.updated_parsed.tm_mon == 12
assert events[0].data.updated_parsed.tm_mday == 13
assert events[0].data.updated_parsed.tm_hour == 18
assert events[0].data.updated_parsed.tm_min == 30
async def test_feed_updates(hass, events, feed_one_event, feed_two_event): async def test_feed_updates(hass, events, feed_one_event, feed_two_event):
"""Test feed updates.""" """Test feed updates."""
side_effect = [ side_effect = [

View File

@ -33,16 +33,6 @@ async def test_import_flow(hass):
"AccessToken": "mock-access-token", "AccessToken": "mock-access-token",
}, },
}, },
), patch(
"homeassistant.components.hive.config_flow.Auth.device_registration",
return_value=True,
), patch(
"homeassistant.components.hive.config_flow.Auth.getDeviceData",
return_value=[
"mock-device-group-key",
"mock-device-key",
"mock-device-password",
],
), patch( ), patch(
"homeassistant.components.hive.async_setup", return_value=True "homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
@ -67,11 +57,6 @@ async def test_import_flow(hass):
}, },
"ChallengeName": "SUCCESS", "ChallengeName": "SUCCESS",
}, },
"device_data": [
"mock-device-group-key",
"mock-device-key",
"mock-device-password",
],
} }
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
@ -96,16 +81,6 @@ async def test_user_flow(hass):
"AccessToken": "mock-access-token", "AccessToken": "mock-access-token",
}, },
}, },
), patch(
"homeassistant.components.hive.config_flow.Auth.device_registration",
return_value=True,
), patch(
"homeassistant.components.hive.config_flow.Auth.getDeviceData",
return_value=[
"mock-device-group-key",
"mock-device-key",
"mock-device-password",
],
), patch( ), patch(
"homeassistant.components.hive.async_setup", return_value=True "homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
@ -130,11 +105,6 @@ async def test_user_flow(hass):
}, },
"ChallengeName": "SUCCESS", "ChallengeName": "SUCCESS",
}, },
"device_data": [
"mock-device-group-key",
"mock-device-key",
"mock-device-password",
],
} }
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1

View File

@ -1256,12 +1256,8 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config):
await mqtt_mock_entry_with_yaml_config() await mqtt_mock_entry_with_yaml_config()
state_attributes_dict = hass.states.get("cover.test").attributes state_attributes_dict = hass.states.get("cover.test").attributes
assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict # Tilt position is not yet known
assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_TILT_POSITION
]
assert current_cover_position == STATE_UNKNOWN
async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config):

View File

@ -168,7 +168,7 @@ async def sensor_fixture(
sensor_obj.motion_detected_at = now - timedelta(hours=1) sensor_obj.motion_detected_at = now - timedelta(hours=1)
sensor_obj.open_status_changed_at = now - timedelta(hours=1) sensor_obj.open_status_changed_at = now - timedelta(hours=1)
sensor_obj.alarm_triggered_at = now - timedelta(hours=1) sensor_obj.alarm_triggered_at = now - timedelta(hours=1)
sensor_obj.tampering_detected_at = now - timedelta(hours=1) sensor_obj.tampering_detected_at = None
mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.reset_objects()
mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
@ -204,7 +204,7 @@ async def sensor_none_fixture(
sensor_obj.mount_type = MountType.LEAK sensor_obj.mount_type = MountType.LEAK
sensor_obj.battery_status.is_low = False sensor_obj.battery_status.is_low = False
sensor_obj.alarm_settings.is_enabled = False sensor_obj.alarm_settings.is_enabled = False
sensor_obj.tampering_detected_at = now - timedelta(hours=1) sensor_obj.tampering_detected_at = None
mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.reset_objects()
mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.nvr.system_info.storage.devices = []

18
tests/fixtures/feedreader5.xml vendored Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<feed
xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<link href="http://example.org/"/>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>