Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent tplink missing devices and unavailable state #39762

Merged
merged 47 commits into from
Oct 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
bdc7346
Adds self to codeowners for tplink
TheGardenMonkey Sep 7, 2020
ef85725
Adds retry to update to prevent missing devices
TheGardenMonkey Sep 7, 2020
91f6730
Runs through isort and corrects async commit
TheGardenMonkey Sep 7, 2020
eef077a
Runs through black
TheGardenMonkey Sep 7, 2020
476d9b7
Runs through pre-checks
TheGardenMonkey Sep 7, 2020
ec8a410
Corrects and matches var names
TheGardenMonkey Sep 7, 2020
c2f482b
Re-runs through black
TheGardenMonkey Sep 7, 2020
b1ee75d
Corrects var name
TheGardenMonkey Sep 7, 2020
30f537c
Removes the retry loop and in favor of async add
TheGardenMonkey Sep 8, 2020
d0c6777
Cleanup imports
TheGardenMonkey Sep 8, 2020
4131088
Removes no longer valid test
TheGardenMonkey Sep 8, 2020
f6129eb
Removes is_ready and only log retry once
TheGardenMonkey Sep 13, 2020
feef50c
Corrects switch logging vars
TheGardenMonkey Sep 13, 2020
d35fe73
Adds list of entities to add_entities
TheGardenMonkey Sep 13, 2020
40c29b0
Consumes exception for attempt_update
TheGardenMonkey Sep 13, 2020
baaae83
Consumes specific exception
TheGardenMonkey Sep 14, 2020
5492b88
Removes unnecessary update
TheGardenMonkey Sep 14, 2020
982883e
Reducing back to 2 seconds
TheGardenMonkey Sep 15, 2020
2171a72
Removes useless return
TheGardenMonkey Sep 17, 2020
47ff59a
Call get_sysinfo for all at once
TheGardenMonkey Sep 17, 2020
64990d2
Formated black
TheGardenMonkey Sep 17, 2020
1feb514
Adds missing docstirng
TheGardenMonkey Sep 17, 2020
9fb4b34
Corrects docstring
TheGardenMonkey Sep 17, 2020
68327a4
Update homeassistant/components/tplink/light.py
TheGardenMonkey Sep 17, 2020
c70781e
Corrects sysinfo call
TheGardenMonkey Sep 17, 2020
1c18611
Adds default for host vars
TheGardenMonkey Sep 17, 2020
e48df74
Adds log when device responds again
TheGardenMonkey Sep 17, 2020
d0bfa1b
Revert host alias default
TheGardenMonkey Sep 18, 2020
724a210
Removes unncessary host var
TheGardenMonkey Sep 18, 2020
7d5d9f4
Removes host var
TheGardenMonkey Sep 18, 2020
64ffc5a
Get device details from sysinfo
TheGardenMonkey Sep 18, 2020
d4544ee
Use host and alias for log msg
TheGardenMonkey Sep 18, 2020
72e7770
Gets hosts from smartbulb
TheGardenMonkey Sep 18, 2020
3e97ae7
Changes retry logging to debug
TheGardenMonkey Sep 19, 2020
73bff6f
Attempts coverage add
TheGardenMonkey Sep 20, 2020
cf3f4f9
Removes unused import
TheGardenMonkey Sep 20, 2020
a8a1dcc
Updates tests for new retry
TheGardenMonkey Sep 20, 2020
1264f60
Runs through isort
TheGardenMonkey Sep 20, 2020
85ec0c5
Removes unneeded try
TheGardenMonkey Sep 20, 2020
37eedcb
Prevents static entries from failing integration
TheGardenMonkey Oct 10, 2020
39962c2
Merge branch 'dev' into dev
TheGardenMonkey Oct 11, 2020
2be424e
Format black
TheGardenMonkey Oct 11, 2020
80278c7
Forces an update after turn on off
TheGardenMonkey Oct 11, 2020
f4f46f0
Remove common test
TheGardenMonkey Oct 11, 2020
2a71bc0
Revert update after turn_on off
TheGardenMonkey Oct 11, 2020
34a07ae
Adds patch for sleep_time 0
TheGardenMonkey Oct 11, 2020
2583f9f
Returns False when update fails
TheGardenMonkey Oct 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,7 @@ omit =
homeassistant/components/torque/sensor.py
homeassistant/components/totalconnect/*
homeassistant/components/touchline/climate.py
homeassistant/components/tplink/common.py
homeassistant/components/tplink/switch.py
homeassistant/components/tplink_lte/*
homeassistant/components/traccar/device_tracker.py
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ homeassistant/components/tmb/* @alemuro
homeassistant/components/todoist/* @boralyl
homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force
Expand Down
102 changes: 15 additions & 87 deletions homeassistant/components/tplink/common.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""Common code for tplink."""
import asyncio
from datetime import timedelta
import logging
from typing import Any, Callable, List
from typing import List

from pyHS100 import (
Discover,
Expand Down Expand Up @@ -113,89 +111,19 @@ def get_static_devices(config_data) -> SmartDevices:
for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]:
for entry in config_data[type_]:
host = entry["host"]

if type_ == CONF_LIGHT:
lights.append(SmartBulb(host))
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
elif type_ == CONF_STRIP:
try:
ss_host = SmartStrip(host)
except SmartDeviceException as sde:
_LOGGER.error(
"Failed to setup SmartStrip at %s: %s; not retrying", host, sde
)
continue
for plug in ss_host.plugs.values():
switches.append(plug)
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
lights.append(SmartPlug(host))

return SmartDevices(lights, switches)


async def async_add_entities_retry(
hass: HomeAssistantType,
async_add_entities: Callable[[List[Any], bool], None],
objects: List[Any],
callback: Callable[[Any, Callable], None],
interval: timedelta = timedelta(seconds=60),
):
"""
Add entities now and retry later if issues are encountered.

If the callback throws an exception or returns false, that
object will try again a while later.
This is useful for devices that are not online when hass starts.
:param hass:
:param async_add_entities: The callback provided to a
platform's async_setup.
:param objects: The objects to create as entities.
:param callback: The callback that will perform the add.
:param interval: THe time between attempts to add.
:return: A callback to cancel the retries.
"""
add_objects = objects.copy()

is_cancelled = False

def cancel_interval_callback():
nonlocal is_cancelled
is_cancelled = True

async def process_objects_loop(delay: int):
if is_cancelled:
return

await process_objects()

if not add_objects:
return

await asyncio.sleep(delay)

hass.async_create_task(process_objects_loop(delay))

async def process_objects(*args):
# Process each object.
for add_object in list(add_objects):
# Call the individual item callback.
try:
_LOGGER.debug("Attempting to add object of type %s", type(add_object))
result = await hass.async_add_job(
callback, add_object, async_add_entities
if type_ == CONF_LIGHT:
lights.append(SmartBulb(host))
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
elif type_ == CONF_STRIP:
for plug in SmartStrip(host).plugs.values():
switches.append(plug)
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
lights.append(SmartPlug(host))
except SmartDeviceException as sde:
_LOGGER.error(
"Failed to setup device %s due to %s; not retrying", host, sde
)
except SmartDeviceException as ex:
_LOGGER.debug(str(ex))
result = False

if result is True or result is None:
_LOGGER.debug("Added object")
add_objects.remove(add_object)
else:
_LOGGER.debug("Failed to add object, will try again later")

await process_objects_loop(interval.seconds)

return cancel_interval_callback
return SmartDevices(lights, switches)
107 changes: 63 additions & 44 deletions homeassistant/components/tplink/light.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for TPLink lights."""
import asyncio
from datetime import timedelta
import logging
import time
Expand All @@ -25,7 +26,6 @@
import homeassistant.util.dt as dt_util

from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
from .common import async_add_entities_retry

PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=5)
Expand Down Expand Up @@ -54,23 +54,26 @@
LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP = "is_variable_color_temp"
LIGHT_SYSINFO_IS_COLOR = "is_color"

MAX_ATTEMPTS = 300
SLEEP_TIME = 2


async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
"""Set up switches."""
await async_add_entities_retry(
hass, async_add_entities, hass.data[TPLINK_DOMAIN][CONF_LIGHT], add_entity
)
return True
"""Set up lights."""
devices = hass.data[TPLINK_DOMAIN][CONF_LIGHT]
entities = []

await hass.async_add_executor_job(get_devices_sysinfo, devices)
for device in devices:
entities.append(TPLinkSmartBulb(device))

async_add_entities(entities, update_before_add=True)

def add_entity(device: SmartBulb, async_add_entities):
"""Check if device is online and add the entity."""
# Attempt to get the sysinfo. If it fails, it will raise an
# exception that is caught by async_add_entities_retry which
# will try again later.
device.get_sysinfo()

async_add_entities([TPLinkSmartBulb(device)], update_before_add=True)
def get_devices_sysinfo(devices):
"""Get sysinfo for all devices."""
for device in devices:
device.get_sysinfo()


def brightness_to_percentage(byt):
Expand Down Expand Up @@ -134,6 +137,9 @@ def __init__(self, smartbulb: SmartBulb) -> None:
self._last_historical_power_update = None
self._emeter_params = {}

self._host = None
self._alias = None

@property
def unique_id(self):
"""Return a unique ID."""
Expand Down Expand Up @@ -235,40 +241,36 @@ def is_on(self):
"""Return True if device is on."""
return self._light_state.state

def update(self):
"""Update the TP-Link Bulb's state."""
def attempt_update(self, update_attempt):
"""Attempt to get details the TP-Link bulb."""
# State is currently being set, ignore.
if self._is_setting_light_state:
return
return False

try:
# Update light features only once.
if not self._light_features:
self._light_features = self._get_light_features_retry()
self._light_state = self._get_light_state_retry()
self._is_available = True
self._light_features = self._get_light_features()
self._alias = self._light_features.alias
self._host = self.smartbulb.host
self._light_state = self._get_light_state()
return True

except (SmartDeviceException, OSError) as ex:
if self._is_available:
_LOGGER.warning(
"Could not read data for %s: %s", self.smartbulb.host, ex
if update_attempt == 0:
_LOGGER.debug(
"Retrying in %s seconds for %s|%s due to: %s",
SLEEP_TIME,
self._host,
self._alias,
ex,
)
self._is_available = False
return False

@property
def supported_features(self):
"""Flag supported features."""
return self._light_features.supported_features

def _get_light_features_retry(self) -> LightFeatures:
"""Retry the retrieval of the supported features."""
try:
return self._get_light_features()
except (SmartDeviceException, OSError):
pass

_LOGGER.debug("Retrying getting light features")
return self._get_light_features()

def _get_light_features(self):
"""Determine all supported features in one go."""
sysinfo = self.smartbulb.sys_info
Expand Down Expand Up @@ -304,16 +306,6 @@ def _get_light_features(self):
has_emeter=has_emeter,
)

def _get_light_state_retry(self) -> LightState:
"""Retry the retrieval of getting light states."""
try:
return self._get_light_state()
except (SmartDeviceException, OSError):
pass

_LOGGER.debug("Retrying getting light state")
return self._get_light_state()

def _light_state_from_params(self, light_state_params) -> LightState:
brightness = None
color_temp = None
Expand Down Expand Up @@ -474,6 +466,33 @@ def _set_device_state(self, state):

return self._get_device_state()

async def async_update(self):
"""Update the TP-Link bulb's state."""
for update_attempt in range(MAX_ATTEMPTS):
amelchio marked this conversation as resolved.
Show resolved Hide resolved
is_ready = await self.hass.async_add_executor_job(
self.attempt_update, update_attempt
)

if is_ready:
self._is_available = True
if update_attempt > 0:
_LOGGER.debug(
"Device %s|%s responded after %s attempts",
self._host,
self._alias,
update_attempt,
)
break
await asyncio.sleep(SLEEP_TIME)
else:
if self._is_available:
_LOGGER.warning(
"Could not read state for %s|%s",
self._host,
self._alias,
)
self._is_available = False


def _light_state_diff(old_light_state: LightState, new_light_state: LightState):
old_state_param = old_light_state.to_param()
Expand Down
11 changes: 8 additions & 3 deletions homeassistant/components/tplink/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"name": "TP-Link Kasa Smart",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tplink",
"requirements": ["pyHS100==0.3.5.1"],
"codeowners": ["@rytilahti"]
}
"requirements": [
"pyHS100==0.3.5.1"
],
"codeowners": [
"@rytilahti",
"@thegardenmonkey"
]
}
Loading