Reconnecting client: optional keep-alive & timeout

- lirc timeout, to reliably detect disconnect
This commit is contained in:
Martin Bauer 2020-07-13 11:32:38 +00:00
parent feeefbe987
commit 349861ad7c
3 changed files with 66 additions and 31 deletions

View File

@ -2,7 +2,9 @@
import logging
import voluptuous as vol
from datetime import timedelta
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.const import CONF_HOST, CONF_PORT
from ..reconnecting_client import ReconnectingClient
@ -16,6 +18,8 @@ REPEAT_COUNTER = 'repeat_counter'
LIRC_HOST = 'host'
DATA_LIRC_NETWORK = 'data_lirc_network'
LIRC_TIMEOUT = 40 # in seconds
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(vol.All([{
vol.Required(CONF_HOST): cv.string,
@ -39,7 +43,17 @@ class LircConnection(ReconnectingClient):
def __init__(self, hass, config):
super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "lirc_network",
receive_line_callback=self._process_line,
connection_status_changed_callback=self._connection_state_changed)
connection_status_changed_callback=self._connection_state_changed,
timeout=LIRC_TIMEOUT)
self._multiline_response_in_progress = False
self._multiline_response = ""
async def keep_alive_callback(*args, **kwargs):
if self._writer and self.connected:
self._writer.write("VERSION\n".encode())
await self._writer.drain()
async_track_time_interval(hass, keep_alive_callback, timedelta(seconds=LIRC_TIMEOUT / 2))
async def _connection_state_changed(self, _):
pass
@ -48,17 +62,27 @@ class LircConnection(ReconnectingClient):
# Example msg:
# 0000000000001795 00 Down Hauppauge_350
# 0000000000001795 01 Down Hauppauge_350
splitted_line = line.split()
if len(splitted_line) != 4:
_LOGGER.warning(f'Ignoring LIRC message from host {self._host}: "{line}"')
return
if line.startswith("Unknown LIRC Command received: VERSION"):
pass # response of irserver to VERSION query
elif line.strip() == "BEGIN":
self._multiline_response_in_progress = True
elif self._multiline_response_in_progress and line.strip() == "END":
self._multiline_response_in_progress = False
self._multiline_response = ""
elif self._multiline_response_in_progress:
self._multiline_response += line
else:
code, repeat_counter, key_name, remote_name = splitted_line
repeat_counter = int(repeat_counter, 16) # repeat code is hexadecimal
key_name = key_name.lower()
data = {BUTTON_NAME: key_name,
REMOTE_NAME: remote_name,
REPEAT_COUNTER: repeat_counter,
LIRC_HOST: self._host}
_LOGGER.info(f"Got new LIRC network code {data}")
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED, data)
splitted_line = line.split()
if len(splitted_line) != 4:
_LOGGER.warning(f'Ignoring LIRC message from host {self._host}: "{line}"')
return
else:
code, repeat_counter, key_name, remote_name = splitted_line
repeat_counter = int(repeat_counter, 16) # repeat code is hexadecimal
key_name = key_name.lower()
data = {BUTTON_NAME: key_name,
REMOTE_NAME: remote_name,
REPEAT_COUNTER: repeat_counter,
LIRC_HOST: self._host}
_LOGGER.info(f"Got new LIRC network code {data}")
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED, data)

View File

@ -2,6 +2,7 @@ import asyncio
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.binary_sensor import BinarySensorDevice
import socket
class IsConnectedSensor(BinarySensorDevice):
@ -12,16 +13,16 @@ class IsConnectedSensor(BinarySensorDevice):
async def set_value(self, value):
self._on = value
await self.async_update_ha_state()
# this doesn't work sometimes - but polling is enabled anyway
try:
await asyncio.wait_for(self.async_update_ha_state(), 1)
except Exception:
pass
@property
def name(self):
return self._name
@property
def should_poll(self):
return False
@property
def available(self) -> bool:
return True
@ -30,13 +31,20 @@ class IsConnectedSensor(BinarySensorDevice):
def is_on(self):
return self._on
def refresh(self):
pass
def update(self):
pass
_LOGGER = logging.getLogger(__name__)
class ReconnectingClient:
def __init__(self, hass, host, port, connection_name, receive_line_callback, connection_status_changed_callback):
def __init__(self, hass, host, port, connection_name, receive_line_callback,
connection_status_changed_callback, timeout=None):
self.connected = False
self.reconnect_time_start = 1
self.reconnect_time_max = 60
@ -52,6 +60,7 @@ class ReconnectingClient:
self._connection_name = connection_name
self._connection_task = None
self._connected_sensor = None
self._timeout = timeout
@property
def connected_sensor(self):
@ -100,23 +109,25 @@ class ReconnectingClient:
self.reconnect_time = self.reconnect_time_start
while self._run:
line = await reader.readline()
if self._timeout:
line = await asyncio.wait_for(reader.readline(), self._timeout)
else:
line = await reader.readline()
if not line:
raise OSError("Disconnect")
line = line.decode()
_LOGGER.debug(f"{self._connection_name} received line - passing along '{line}'")
await self._receive_line_callback(line)
except OSError as e:
except (OSError, asyncio.TimeoutError):
if self._connection_last_state != 'FAILED':
notification_text = "{} connection to {}:{} failed".format(self._connection_name, self._host,
self._port)
notification_text = f"{self._connection_name} connection to {self._host}:{self._port} failed"
self.hass.components.persistent_notification.async_create(notification_text, title="No connection")
_LOGGER.error("Connection to {} failed {}:{}".format(self._connection_name, self._host, self._port))
_LOGGER.error(f"Connection to {self._connection_name} failed {self._host}:{self._port}")
await self._connection_status_changed_callback('disconnected')
if self._connected_sensor:
await self._connected_sensor.set_value(False)
_LOGGER.error(
"After connection failed ({} {}:{})".format(self._connection_name, self._host, self._port))
await asyncio.wait_for(self._connected_sensor.set_value(False), timeout=2.0)
_LOGGER.error(f"After setting sensor ({self._connection_name} {self._host}:{self._port})")
_LOGGER.error(f"After connection failed ({self._connection_name} {self._host}:{self._port})")
else:
_LOGGER.debug(f"{self._connection_name} retried connection, last state {self._connection_last_state}"
f"reconnection time {self.reconnect_time}")

View File

@ -441,9 +441,6 @@ class SqueezeBoxDevice(MediaPlayerDevice):
"""Flag media player features that are supported."""
return SUPPORT_SQUEEZEBOX
def turn_off(self):
self.call_method('power', '0')
def volume_up(self):
self.call_method('mixer', 'volume', '+5')
@ -479,6 +476,9 @@ class SqueezeBoxDevice(MediaPlayerDevice):
def turn_on(self):
self.call_method('power', '1')
def turn_off(self):
self.call_method('power', '0')
def play_media(self, media_type, media_id, **kwargs):
"""
Send the play_media command to the media player.