Compare commits

...

24 Commits

Author SHA1 Message Date
Paulus Schoutsen
076227acbe Merge pull request #52043 from home-assistant/rc 2021-06-20 22:37:31 -07:00
Paulus Schoutsen
34f266bfa6 Bumped version to 2021.6.6 2021-06-20 21:49:17 -07:00
jjlawren
0d351e4a0e Catch unexpected battery update payloads on Sonos (#52040) 2021-06-20 21:49:12 -07:00
Paulus Schoutsen
79cfd444d9 Fix double subscriptions for local push notifications (#52039) 2021-06-20 21:49:11 -07:00
Maciej Bieniek
67699e3c1f Fix AccuWeather sensors updates (#52031)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-06-20 21:49:10 -07:00
Fredrik Erlandsson
47560006b8 Bump pydaikin to 2.4.3 (#51926) 2021-06-20 21:49:09 -07:00
Rob Bierbooms
d6fd41bd03 Bump pyRFXtrx to 0.27.0 (#51911)
* Bump version

* Fix test
2021-06-20 21:49:08 -07:00
djtimca
c8e678a2c6 Add Omnilogic switch defaults for max_speed and min_speed (#51889) 2021-06-20 21:49:08 -07:00
Konstantin Antselovich
398fca3b9d Fix whois expiration date (#51868) 2021-06-20 21:49:07 -07:00
Franck Nijhof
f2bc69a653 Merge pull request #51902 from home-assistant/rc 2021-06-15 20:37:40 +02:00
Franck Nijhof
fcb2c26a24 Bumped version to 2021.6.5 2021-06-15 19:45:33 +02:00
Ludovico de Nittis
ee7a2b29ad Bump pyialarm to 1.9.0 (#51804) 2021-06-15 19:44:48 +02:00
Joakim Sørensen
dcc7bc10e8 Add httpcore with version 0.13.3 (#51799) 2021-06-15 19:44:45 +02:00
Fredrik Erlandsson
8f34d1cf32 Bump pydaikin, fix airbase issues (#51797) 2021-06-15 19:44:42 +02:00
Ron Klinkien
fcc66139f8 Replace garminconnect_aio with garminconnect_ha (#51730)
* Fixed config_flow for multiple account creation

* Replaced python package to fix multiple accounts

* Replaced python package to fix multiple accounts

* Implemented config entries user

* Config entries user

* Fixed test code config flow

* Fixed patch
2021-06-15 19:44:35 +02:00
Paulus Schoutsen
2535f5c155 Merge pull request #51768 from home-assistant/rc 2021-06-11 22:18:37 -07:00
Paulus Schoutsen
97e36cd3c4 Bumped version to 2021.6.4 2021-06-11 21:42:27 -07:00
J. Nick Koston
548e847453 Fix race condition in samsungtv turn off (#51716)
- The state would flip flop if the update happened before the TV had fully shutdown
2021-06-11 21:42:15 -07:00
J. Nick Koston
60b89101e5 Ensure samsungtv reloads after reauth (#51714)
* Ensure samsungtv reloads after reauth

- Fixes a case of I/O in the event loop

* Ensure config entry is reloaded
2021-06-11 21:42:14 -07:00
Pawel
90d28e911c Fix Onvif get_time_zone from device (#51620)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-06-11 21:42:13 -07:00
Maciej Bieniek
cfea8a9ad1 Do not configure Shelly config entry created by custom component (#51616) 2021-06-11 21:42:12 -07:00
blastoise186
880fe82337 Reduce ovo_energy polling rate to be less aggressive (#51613)
* Reduce polling rate to be less aggressive

The current polling rate is too aggressive for the purpose, this commit reduces it to 12 hours to play nice with OVO.

* tweak polling to hourly
2021-06-11 21:42:11 -07:00
jjlawren
3a5f51ed7d Handle missing section ID for Plex clips (#51598) 2021-06-11 21:42:10 -07:00
jjlawren
cca1b426bb Fix Sonos battery sensors on S1 firmware (#51585) 2021-06-11 21:42:09 -07:00
29 changed files with 259 additions and 123 deletions

View File

@@ -12,7 +12,7 @@ from homeassistant.const import (
CONF_NAME,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -81,16 +81,11 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind)
if forecast_day is None:
self._description = SENSOR_TYPES[kind]
self._sensor_data: dict[str, Any]
if kind == "Precipitation":
self._sensor_data = coordinator.data["PrecipitationSummary"][kind]
else:
self._sensor_data = coordinator.data[kind]
else:
self._description = FORECAST_SENSOR_TYPES[kind]
self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind]
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._name = name
self.kind = kind
@@ -182,3 +177,24 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._description[ATTR_ENABLED]
@callback
def _handle_coordinator_update(self) -> None:
"""Handle data update."""
self._sensor_data = _get_sensor_data(
self.coordinator.data, self.forecast_day, self.kind
)
self.async_write_ha_state()
def _get_sensor_data(
sensors: dict[str, Any], forecast_day: int | None, kind: str
) -> Any:
"""Get sensor data."""
if forecast_day is not None:
return sensors[ATTR_FORECAST][forecast_day][kind]
if kind == "Precipitation":
return sensors["PrecipitationSummary"][kind]
return sensors[kind]

View File

@@ -3,7 +3,7 @@
"name": "Daikin AC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin",
"requirements": ["pydaikin==2.4.1"],
"requirements": ["pydaikin==2.4.3"],
"codeowners": ["@fredrike"],
"zeroconf": ["_dkapi._tcp.local."],
"quality_scale": "platinum",

View File

@@ -2,7 +2,7 @@
from datetime import date
import logging
from garminconnect_aio import (
from garminconnect_ha import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
@@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN
@@ -26,14 +25,13 @@ PLATFORMS = ["sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Garmin Connect from a config entry."""
websession = async_get_clientsession(hass)
username: str = entry.data[CONF_USERNAME]
password: str = entry.data[CONF_PASSWORD]
garmin_client = Garmin(websession, username, password)
api = Garmin(username, password)
try:
await garmin_client.login()
await hass.async_add_executor_job(api.login)
except (
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
@@ -49,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.exception("Unknown error occurred during Garmin Connect login request")
return False
garmin_data = GarminConnectData(hass, garmin_client)
garmin_data = GarminConnectData(hass, api)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = garmin_data
@@ -81,14 +79,20 @@ class GarminConnectData:
today = date.today()
try:
summary = await self.client.get_user_summary(today.isoformat())
body = await self.client.get_body_composition(today.isoformat())
summary = await self.hass.async_add_executor_job(
self.client.get_user_summary, today.isoformat()
)
body = await self.hass.async_add_executor_job(
self.client.get_body_composition, today.isoformat()
)
self.data = {
**summary,
**body["totalAverage"],
}
self.data["nextAlarm"] = await self.client.get_device_alarms()
self.data["nextAlarm"] = await self.hass.async_add_executor_job(
self.client.get_device_alarms
)
except (
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,

View File

@@ -1,7 +1,7 @@
"""Config flow for Garmin Connect integration."""
import logging
from garminconnect_aio import (
from garminconnect_ha import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
@@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
@@ -38,15 +37,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None:
return await self._show_setup_form()
websession = async_get_clientsession(self.hass)
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
garmin_client = Garmin(websession, username, password)
api = Garmin(username, password)
errors = {}
try:
await garmin_client.login()
await self.hass.async_add_executor_job(api.login)
except GarminConnectConnectionError:
errors["base"] = "cannot_connect"
return await self._show_setup_form(errors)

View File

@@ -2,8 +2,8 @@
"domain": "garmin_connect",
"name": "Garmin Connect",
"documentation": "https://www.home-assistant.io/integrations/garmin_connect",
"requirements": ["garminconnect_aio==0.1.4"],
"requirements": ["garminconnect_ha==0.1.6"],
"codeowners": ["@cyberjunky"],
"config_flow": true,
"iot_class": "cloud_polling"
}
}

View File

@@ -2,7 +2,7 @@
"domain": "ialarm",
"name": "Antifurto365 iAlarm",
"documentation": "https://www.home-assistant.io/integrations/ialarm",
"requirements": ["pyialarm==1.8.1"],
"requirements": ["pyialarm==1.9.0"],
"codeowners": ["@RyuzakiKK"],
"config_flow": true,
"iot_class": "local_polling"

View File

@@ -160,7 +160,7 @@ def handle_push_notification_channel(hass, connection, msg):
registered_channels = hass.data[DOMAIN][DATA_PUSH_CHANNEL]
if webhook_id in registered_channels:
registered_channels.pop(webhook_id)()
registered_channels.pop(webhook_id)
@callback
def forward_push_notification(data):

View File

@@ -153,8 +153,8 @@ class OmniLogicPumpControl(OmniLogicSwitch):
state_key=state_key,
)
self._max_speed = int(coordinator.data[item_id]["Max-Pump-Speed"])
self._min_speed = int(coordinator.data[item_id]["Min-Pump-Speed"])
self._max_speed = int(coordinator.data[item_id].get("Max-Pump-Speed", 100))
self._min_speed = int(coordinator.data[item_id].get("Min-Pump-Speed", 0))
if "Filter-Type" in coordinator.data[item_id]:
self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Filter-Type"]]

View File

@@ -169,7 +169,9 @@ class ONVIFDevice:
cdate = device_time.UTCDateTime
else:
tzone = (
dt_util.get_time_zone(device_time.TimeZone)
dt_util.get_time_zone(
device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE)
)
or dt_util.DEFAULT_TIME_ZONE
)
cdate = device_time.LocalDateTime

View File

@@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
name="sensor",
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=300),
update_interval=timedelta(seconds=3600),
)
hass.data.setdefault(DOMAIN, {})

View File

@@ -78,7 +78,7 @@ class PlexSession:
if media.librarySectionID in SPECIAL_SECTIONS:
self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID]
elif media.librarySectionID < 1:
elif media.librarySectionID and media.librarySectionID < 1:
self.media_library_title = UNKNOWN_SECTION
_LOGGER.warning(
"Unknown library section ID (%s) for title '%s', please create an issue",

View File

@@ -2,7 +2,7 @@
"domain": "rfxtrx",
"name": "RFXCOM RFXtrx",
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
"requirements": ["pyRFXtrx==0.26.1"],
"requirements": ["pyRFXtrx==0.27.0"],
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
"config_flow": true,
"iot_class": "local_push"

View File

@@ -291,13 +291,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
bridge = SamsungTVBridge.get_bridge(
self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST]
)
result = bridge.try_connect()
result = await self.hass.async_add_executor_job(bridge.try_connect)
if result == RESULT_SUCCESS:
new_data = dict(self._reauth_entry.data)
new_data[CONF_TOKEN] = bridge.token
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=new_data
)
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT):
return self.async_abort(reason=result)

View File

@@ -21,6 +21,7 @@ from homeassistant.components.media_player.const import (
)
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.helpers import entity_component
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.script import Script
@@ -50,6 +51,13 @@ SUPPORT_SAMSUNGTV = (
| SUPPORT_PLAY_MEDIA
)
# Since the TV will take a few seconds to go to sleep
# and actually be seen as off, we need to wait just a bit
# more than the next scan interval
SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta(
seconds=5
)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Samsung TV from a config entry."""
@@ -148,7 +156,12 @@ class SamsungTVDevice(MediaPlayerEntity):
"""Return the availability of the device."""
if self._auth_failed:
return False
return self._state == STATE_ON or self._on_script or self._mac
return (
self._state == STATE_ON
or self._on_script
or self._mac
or self._power_off_in_progress()
)
@property
def device_info(self):
@@ -187,7 +200,7 @@ class SamsungTVDevice(MediaPlayerEntity):
def turn_off(self):
"""Turn off media player."""
self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15)
self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME
self.send_key("KEY_POWEROFF")
# Force closing of remote session to provide instant UI feedback

View File

@@ -68,6 +68,18 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Shelly from a config entry."""
# The custom component for Shelly devices uses shelly domain as well as core
# integration. If the user removes the custom component but doesn't remove the
# config entry, core integration will try to configure that config entry with an
# error. The config entry data for this custom component doesn't contain host
# value, so if host isn't present, config entry will not be configured.
if not entry.data.get(CONF_HOST):
_LOGGER.warning(
"The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration",
entry.title,
)
return False
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None

View File

@@ -173,7 +173,7 @@ class SonosSpeaker:
self.zone_name = speaker_info["zone_name"]
# Battery
self.battery_info: dict[str, Any] | None = None
self.battery_info: dict[str, Any] = {}
self._last_battery_event: datetime.datetime | None = None
self._battery_poll_timer: Callable | None = None
@@ -208,21 +208,15 @@ class SonosSpeaker:
self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen
)
if (battery_info := fetch_battery_info_or_none(self.soco)) is None:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
else:
if battery_info := fetch_battery_info_or_none(self.soco):
self.battery_info = battery_info
# Only create a polling task if successful, may fail on S1 firmware
if battery_info:
# Battery events can be infrequent, polling is still necessary
self._battery_poll_timer = self.hass.helpers.event.track_time_interval(
self.async_poll_battery, BATTERY_SCAN_INTERVAL
)
else:
_LOGGER.warning(
"S1 firmware detected, battery sensor may update infrequently"
)
# Battery events can be infrequent, polling is still necessary
self._battery_poll_timer = self.hass.helpers.event.track_time_interval(
self.async_poll_battery, BATTERY_SCAN_INTERVAL
)
dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self)
else:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
if new_alarms := self.update_alarms_for_speaker():
dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
@@ -386,8 +380,16 @@ class SonosSpeaker:
async def async_update_device_properties(self, event: SonosEvent) -> None:
"""Update device properties from an event."""
if (more_info := event.variables.get("more_info")) is not None:
if more_info := event.variables.get("more_info"):
battery_dict = dict(x.split(":") for x in more_info.split(","))
if "BattChg" not in battery_dict:
_LOGGER.debug(
"Unknown device properties update for %s (%s), please report an issue: '%s'",
self.zone_name,
self.model_name,
more_info,
)
return
await self.async_update_battery_info(battery_dict)
self.async_write_entity_states()
@@ -514,12 +516,19 @@ class SonosSpeaker:
if not self._battery_poll_timer:
# Battery info received for an S1 speaker
new_battery = not self.battery_info
self.battery_info.update(
{
"Level": int(battery_dict["BattPct"]),
"PowerSource": "EXTERNAL" if is_charging else "BATTERY",
}
)
if new_battery:
_LOGGER.warning(
"S1 firmware detected on %s, battery info may update infrequently",
self.zone_name,
)
async_dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self)
return
if is_charging == self.charging:

View File

@@ -118,6 +118,7 @@ class WhoisSensor(SensorEntity):
expiration_date = response["expiration_date"]
if isinstance(expiration_date, list):
attrs[ATTR_EXPIRES] = expiration_date[0].isoformat()
expiration_date = expiration_date[0]
else:
attrs[ATTR_EXPIRES] = expiration_date.isoformat()

View File

@@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "6"
__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

@@ -63,3 +63,7 @@ enum34==1000000000.0.0
typing==1000000000.0.0
uuid==1000000000.0.0
# httpcore 0.13.4 breaks several integrations
# https://github.com/home-assistant/core/issues/51778
httpcore==0.13.3

View File

@@ -635,7 +635,7 @@ gTTS==2.2.2
garages-amsterdam==2.1.1
# homeassistant.components.garmin_connect
garminconnect_aio==0.1.4
garminconnect_ha==0.1.6
# homeassistant.components.geniushub
geniushub-client==0.6.30
@@ -1253,7 +1253,7 @@ pyMetEireann==0.2
pyMetno==0.8.3
# homeassistant.components.rfxtrx
pyRFXtrx==0.26.1
pyRFXtrx==0.27.0
# homeassistant.components.switchmate
# pySwitchmate==0.4.6
@@ -1352,7 +1352,7 @@ pycsspeechtts==1.0.4
# pycups==1.9.73
# homeassistant.components.daikin
pydaikin==2.4.1
pydaikin==2.4.3
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
@@ -1464,7 +1464,7 @@ pyhomematic==0.1.72
pyhomeworks==0.0.6
# homeassistant.components.ialarm
pyialarm==1.8.1
pyialarm==1.9.0
# homeassistant.components.icloud
pyicloud==0.10.2

View File

@@ -341,7 +341,7 @@ gTTS==2.2.2
garages-amsterdam==2.1.1
# homeassistant.components.garmin_connect
garminconnect_aio==0.1.4
garminconnect_ha==0.1.6
# homeassistant.components.geo_json_events
# homeassistant.components.usgs_earthquakes_feed
@@ -693,7 +693,7 @@ pyMetEireann==0.2
pyMetno==0.8.3
# homeassistant.components.rfxtrx
pyRFXtrx==0.26.1
pyRFXtrx==0.27.0
# homeassistant.components.tibber
pyTibber==0.17.0
@@ -747,7 +747,7 @@ pycomfoconnect==0.4
pycoolmasternet-async==0.1.2
# homeassistant.components.daikin
pydaikin==2.4.1
pydaikin==2.4.3
# homeassistant.components.deconz
pydeconz==79
@@ -808,7 +808,7 @@ pyhiveapi==0.4.2
pyhomematic==0.1.72
# homeassistant.components.ialarm
pyialarm==1.8.1
pyialarm==1.9.0
# homeassistant.components.icloud
pyicloud==0.10.2

View File

@@ -83,6 +83,10 @@ enum34==1000000000.0.0
typing==1000000000.0.0
uuid==1000000000.0.0
# httpcore 0.13.4 breaks several integrations
# https://github.com/home-assistant/core/issues/51778
httpcore==0.13.3
"""
IGNORE_PRE_COMMIT_HOOK_ID = (

View File

@@ -673,3 +673,36 @@ async def test_sensor_imperial_units(hass):
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET
async def test_state_update(hass):
"""Ensure the sensor state changes after updating the data."""
await init_integration(hass)
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "3200"
future = utcnow() + timedelta(minutes=60)
current_condition = json.loads(
load_fixture("accuweather/current_conditions_data.json")
)
current_condition["Ceiling"]["Metric"]["Value"] = 3300
with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current_condition,
), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "3300"

View File

@@ -1,12 +1,11 @@
"""Test the Garmin Connect config flow."""
from unittest.mock import patch
from garminconnect_aio import (
from garminconnect_ha import (
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.garmin_connect.const import DOMAIN
@@ -21,37 +20,23 @@ MOCK_CONF = {
}
@pytest.fixture(name="mock_garmin_connect")
def mock_garmin():
"""Mock Garmin Connect."""
with patch(
"homeassistant.components.garmin_connect.config_flow.Garmin",
) as garmin:
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
yield garmin.return_value
async def test_show_form(hass):
"""Test that the form is served with no input."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
assert result["step_id"] == config_entries.SOURCE_USER
async def test_step_user(hass):
"""Test registering an integration and finishing flow works."""
with patch(
"homeassistant.components.garmin_connect.Garmin.login",
return_value="my@email.address",
), patch(
"homeassistant.components.garmin_connect.async_setup_entry", return_value=True
):
), patch(
"homeassistant.components.garmin_connect.config_flow.Garmin",
) as garmin:
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
)
@@ -59,60 +44,69 @@ async def test_step_user(hass):
assert result["data"] == MOCK_CONF
async def test_connection_error(hass, mock_garmin_connect):
async def test_connection_error(hass):
"""Test for connection error."""
mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
)
with patch(
"homeassistant.components.garmin_connect.Garmin.login",
side_effect=GarminConnectConnectionError("errormsg"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_CONF
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
async def test_authentication_error(hass, mock_garmin_connect):
async def test_authentication_error(hass):
"""Test for authentication error."""
mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
)
with patch(
"homeassistant.components.garmin_connect.Garmin.login",
side_effect=GarminConnectAuthenticationError("errormsg"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_CONF
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_auth"}
async def test_toomanyrequest_error(hass, mock_garmin_connect):
async def test_toomanyrequest_error(hass):
"""Test for toomanyrequests error."""
mock_garmin_connect.login.side_effect = GarminConnectTooManyRequestsError(
"errormsg"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
)
with patch(
"homeassistant.components.garmin_connect.Garmin.login",
side_effect=GarminConnectTooManyRequestsError("errormsg"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_CONF
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "too_many_requests"}
async def test_unknown_error(hass, mock_garmin_connect):
async def test_unknown_error(hass):
"""Test for unknown error."""
mock_garmin_connect.login.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
)
with patch(
"homeassistant.components.garmin_connect.Garmin.login",
side_effect=Exception,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_CONF
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "unknown"}
async def test_abort_if_already_setup(hass):
"""Test abort if already setup."""
MockConfigEntry(
domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]
).add_to_hass(hass)
with patch(
"homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True
) as garmin:
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
"homeassistant.components.garmin_connect.config_flow.Garmin",
):
entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_CONF
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"

View File

@@ -136,6 +136,18 @@ async def test_notify_ws_works(
sub_result = await client.receive_json()
assert sub_result["success"]
# Subscribe twice, it should forward all messages to 2nd subscription
await client.send_json(
{
"id": 6,
"type": "mobile_app/push_notification_channel",
"webhook_id": "mock-webhook_id",
}
)
sub_result = await client.receive_json()
assert sub_result["success"]
assert await hass.services.async_call(
"notify", "mobile_app_test", {"message": "Hello world"}, blocking=True
)
@@ -144,13 +156,14 @@ async def test_notify_ws_works(
msg_result = await client.receive_json()
assert msg_result["event"] == {"message": "Hello world"}
assert msg_result["id"] == 6 # This is the new subscription
# Unsubscribe, now it should go over http
await client.send_json(
{
"id": 6,
"id": 7,
"type": "unsubscribe_events",
"subscription": 5,
"subscription": 6,
}
)
sub_result = await client.receive_json()
@@ -165,7 +178,7 @@ async def test_notify_ws_works(
# Test non-existing webhook ID
await client.send_json(
{
"id": 7,
"id": 8,
"type": "mobile_app/push_notification_channel",
"webhook_id": "non-existing",
}
@@ -180,7 +193,7 @@ async def test_notify_ws_works(
# Test webhook ID linked to other user
await client.send_json(
{
"id": 8,
"id": 9,
"type": "mobile_app/push_notification_channel",
"webhook_id": "webhook_id_2",
}

View File

@@ -117,7 +117,7 @@ async def test_fire_event(hass, rfxtrx):
"type_string": "Byron SX",
"id_string": "00:90",
"data": "0716000100900970",
"values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9},
"values": {"Command": "Sound 9", "Rssi numeric": 7, "Sound": 9},
"device_id": device_id_2.id,
},
]

View File

@@ -905,6 +905,8 @@ async def test_form_reauth_websocket(hass, remotews: Mock):
"""Test reauthenticate websocket."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY)
entry.add_to_hass(hass)
assert entry.state == config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH},
@@ -920,6 +922,7 @@ async def test_form_reauth_websocket(hass, remotews: Mock):
await hass.async_block_till_done()
assert result2["type"] == "abort"
assert result2["reason"] == "reauth_successful"
assert entry.state == config_entries.ConfigEntryState.LOADED
async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock):

View File

@@ -419,6 +419,18 @@ async def test_state_without_turnon(hass, remote):
assert await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True
)
state = hass.states.get(ENTITY_ID_NOTURNON)
# Should be STATE_UNAVAILABLE after the timer expires
assert state.state == STATE_OFF
next_update = dt_util.utcnow() + timedelta(seconds=20)
with patch(
"homeassistant.components.samsungtv.bridge.Remote",
side_effect=OSError,
), patch("homeassistant.util.dt.utcnow", return_value=next_update):
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID_NOTURNON)
# Should be STATE_UNAVAILABLE since there is no way to turn it back on
assert state.state == STATE_UNAVAILABLE

View File

@@ -3,7 +3,7 @@ from pysonos.exceptions import NotSupportedException
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
@@ -68,21 +68,38 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event):
entity_registry = await hass.helpers.entity_registry.async_get_registry()
battery = entity_registry.entities["sensor.zone_a_battery"]
battery_state = hass.states.get(battery.entity_id)
assert battery_state.state == STATE_UNAVAILABLE
power = entity_registry.entities["binary_sensor.zone_a_power"]
power_state = hass.states.get(power.entity_id)
assert power_state.state == STATE_UNAVAILABLE
assert "sensor.zone_a_battery" not in entity_registry.entities
assert "binary_sensor.zone_a_power" not in entity_registry.entities
# Update the speaker with a callback event
sub_callback(battery_event)
await hass.async_block_till_done()
battery = entity_registry.entities["sensor.zone_a_battery"]
battery_state = hass.states.get(battery.entity_id)
assert battery_state.state == "100"
power = entity_registry.entities["binary_sensor.zone_a_power"]
power_state = hass.states.get(power.entity_id)
assert power_state.state == STATE_OFF
assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY"
async def test_device_payload_without_battery(
hass, config_entry, config, soco, battery_event, caplog
):
"""Test device properties event update without battery info."""
soco.get_battery_info.return_value = None
await setup_platform(hass, config_entry, config)
subscription = soco.deviceProperties.subscribe.return_value
sub_callback = subscription.callback
bad_payload = "BadKey:BadValue"
battery_event.variables["more_info"] = bad_payload
sub_callback(battery_event)
await hass.async_block_till_done()
assert bad_payload in caplog.text