Factored Client code out & implemented LIRC network listener
This commit is contained in:
parent
20f43f4294
commit
1a746d1438
|
@ -31,17 +31,13 @@ dimmer:
|
|||
# Weather prediction
|
||||
# - platform: yr
|
||||
|
||||
# Text to speech
|
||||
#tts:
|
||||
## - platform: google_translate
|
||||
# language: 'de'
|
||||
# base_url: http://192.168.178.73:8123
|
||||
tts:
|
||||
- platform: watson_tts
|
||||
watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7
|
||||
watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api
|
||||
voice: de-DE_BirgitVoice
|
||||
output_format: audio/flac;rate=44100
|
||||
output_format: audio/flac
|
||||
output_audio_rate: 44100
|
||||
|
||||
knx:
|
||||
rate_limit: 20
|
||||
|
@ -57,6 +53,13 @@ fhem:
|
|||
cul_device_name: CUL_HM
|
||||
|
||||
|
||||
lirc_network:
|
||||
- host: kitchenpi
|
||||
port: 2222
|
||||
- host: bedroompi
|
||||
port: 2222
|
||||
|
||||
|
||||
media_player:
|
||||
- platform: squeezebox
|
||||
host: server
|
||||
|
@ -66,7 +69,6 @@ media_player:
|
|||
|
||||
|
||||
|
||||
|
||||
group: !include groups.yaml
|
||||
automation: !include automations.yaml
|
||||
script: !include scripts.yaml
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from ..reconnecting_client import ReconnectingClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_CUL_DEVICE_NAME = 'cul_device_name'
|
||||
|
@ -23,28 +23,20 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
connection = FhemConnection(hass, config)
|
||||
connection = FhemConnection(hass, config[DOMAIN])
|
||||
hass.data[DATA_FHEM] = connection
|
||||
await connection.start()
|
||||
return True
|
||||
|
||||
|
||||
class FhemConnection:
|
||||
class FhemConnection(ReconnectingClient):
|
||||
|
||||
def __init__(self, hass, config):
|
||||
self.hass = hass
|
||||
self._host = config[DOMAIN][CONF_HOST]
|
||||
self._port = config[DOMAIN][CONF_PORT]
|
||||
self._cul_device_name = config[DOMAIN][CONF_CUL_DEVICE_NAME]
|
||||
|
||||
self.connected = False
|
||||
self.reconnect_time_start = 1
|
||||
self.reconnect_time_max = 60
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "FHEM",
|
||||
receive_line_callback=self._process_line,
|
||||
connection_status_changed_callback=self._update_all_devices)
|
||||
self._cul_device_name = config[CONF_CUL_DEVICE_NAME]
|
||||
self._devices = defaultdict(list)
|
||||
self._run = False
|
||||
self._writer = None
|
||||
self._connection_last_state = 'UNKNOWN'
|
||||
|
||||
def register_device(self, id, d):
|
||||
self._devices[id].append(d)
|
||||
|
@ -53,56 +45,11 @@ class FhemConnection:
|
|||
"displayattr .*\n".encode(),
|
||||
])
|
||||
|
||||
async def _update_all_devices(self):
|
||||
async def _update_all_devices(self, _):
|
||||
for device_list in self._devices.values():
|
||||
for device in device_list:
|
||||
await device.async_update_ha_state()
|
||||
|
||||
async def start(self):
|
||||
self._run = True
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
self.hass.loop.create_task(self._connection())
|
||||
|
||||
async def stop(self):
|
||||
self._run = False
|
||||
self.connected = False
|
||||
|
||||
async def _connection(self):
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(self._host, self._port)
|
||||
_LOGGER.info("Connected to FHEM {}:{}".format(self._host, self._port))
|
||||
self._connection_last_state = 'CONNECTED'
|
||||
|
||||
self._writer = writer
|
||||
self.connected = True
|
||||
await self._update_all_devices()
|
||||
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
writer.writelines([
|
||||
"displayattr .*\n".encode(),
|
||||
"inform on\n".encode(),
|
||||
])
|
||||
while self._run:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
_LOGGER.warning("FHEM disconnected: {}".format(line))
|
||||
raise OSError("Disconnect")
|
||||
line = line.decode()
|
||||
_LOGGER.debug("FHEM received line: {}".format(line))
|
||||
await self._process_line(line)
|
||||
except OSError:
|
||||
if self._connection_last_state != 'FAILED':
|
||||
self.hass.components.persistent_notification.async_create("FHEM connection failed",
|
||||
title="No FHEM connection")
|
||||
_LOGGER.error("Connection to FHEM failed {}:{}".format(self._host, self._port))
|
||||
self._connection_last_state = 'FAILED'
|
||||
|
||||
self.connected = False
|
||||
await self._update_all_devices()
|
||||
await asyncio.sleep(self.reconnect_time)
|
||||
self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time_max)
|
||||
self.hass.loop.create_task(self._connection())
|
||||
|
||||
async def _process_line(self, line):
|
||||
if line.startswith(self._cul_device_name + " "): # Status update message
|
||||
_, device_name, command = line.split(" ", 2)
|
||||
|
@ -127,7 +74,7 @@ class FhemConnection:
|
|||
:param arguments: string or list of strings containing command parameters
|
||||
"""
|
||||
arguments = " ".join([str(a) for a in arguments])
|
||||
self._writer.write("set {} {}\n".format(id, arguments).encode())
|
||||
self.write_line("set {} {}\n".format(id, arguments))
|
||||
|
||||
|
||||
def device_error_reporting(hass, received_line, component_type, component_name):
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
"""Integration of lirc network service"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.lirc import EVENT_IR_COMMAND_RECEIVED, BUTTON_NAME
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from ..reconnecting_client import ReconnectingClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'lirc_network'
|
||||
REMOTE_NAME = 'remote'
|
||||
REPEAT_COUNTER = 'repeat_counter'
|
||||
LIRC_HOST = 'host'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema(vol.All([{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=8765): cv.port,
|
||||
}]))
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
_LOGGER.warning(f"Config is {config[DOMAIN]}")
|
||||
for config_element in config[DOMAIN]:
|
||||
connection = LircConnection(hass, config_element)
|
||||
await connection.start()
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
|
||||
async def _connection_state_changed(self, _):
|
||||
pass
|
||||
|
||||
async def _process_line(self, line):
|
||||
# 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
|
||||
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)
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"domain": "lirc_network",
|
||||
"name": "Lirc Network",
|
||||
"documentation": "",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@mabau"]
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReconnectingClient:
|
||||
|
||||
def __init__(self, hass, host, port, connection_name, receive_line_callback, connection_status_changed_callback):
|
||||
self.connected = False
|
||||
self.reconnect_time_start = 1
|
||||
self.reconnect_time_max = 60
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
self.hass = hass
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._receive_line_callback = receive_line_callback
|
||||
self._connection_status_changed_callback = connection_status_changed_callback
|
||||
self._run = False
|
||||
self._writer = None
|
||||
self._connection_last_state = 'UNKNOWN'
|
||||
self._connection_name = connection_name
|
||||
|
||||
async def start(self):
|
||||
self._run = True
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
self.hass.loop.create_task(self._connection())
|
||||
|
||||
async def stop(self):
|
||||
self._run = False
|
||||
self.connected = False
|
||||
|
||||
def write_line(self, line):
|
||||
if self._writer:
|
||||
line += '\n'
|
||||
self._writer.write(line.encode())
|
||||
|
||||
async def _connection(self):
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(self._host, self._port)
|
||||
_LOGGER.info("Connected to {} {}:{}".format(self._connection_name, self._host, self._port))
|
||||
self._connection_last_state = 'CONNECTED'
|
||||
|
||||
self._writer = writer
|
||||
self.connected = True
|
||||
await self._connection_status_changed_callback('connected')
|
||||
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
writer.writelines([
|
||||
"displayattr .*\n".encode(),
|
||||
"inform on\n".encode(),
|
||||
])
|
||||
while self._run:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
raise OSError("Disconnect")
|
||||
line = line.decode()
|
||||
_LOGGER.debug("{} received line: {}".format(self._connection_name, line))
|
||||
await self._receive_line_callback(line)
|
||||
except OSError:
|
||||
if self._connection_last_state != 'FAILED':
|
||||
notification_text = "{} connection to {}:{} failed".format(self._connection_name,self._host, self._port)
|
||||
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))
|
||||
await self._connection_status_changed_callback('disconnected')
|
||||
self._connection_last_state = 'FAILED'
|
||||
|
||||
self.connected = False
|
||||
await asyncio.sleep(self.reconnect_time)
|
||||
self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time_max)
|
||||
self.hass.loop.create_task(self._connection())
|
|
@ -0,0 +1 @@
|
|||
"""Support for IBM Watson TTS integration."""
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"domain": "watson_tts",
|
||||
"name": "IBM Watson TTS",
|
||||
"documentation": "https://www.home-assistant.io/components/watson_tts",
|
||||
"requirements": [
|
||||
"ibm-watson==3.0.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@rutkai"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
"""Support for IBM Watson TTS integration."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import PLATFORM_SCHEMA, Provider
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_URL = 'watson_url'
|
||||
CONF_APIKEY = 'watson_apikey'
|
||||
ATTR_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_URL = 'https://stream.watsonplatform.net/text-to-speech/api'
|
||||
|
||||
CONF_VOICE = 'voice'
|
||||
CONF_OUTPUT_FORMAT = 'output_format'
|
||||
CONF_OUTPUT_AUDIO_RATE = 'output_audio_rate'
|
||||
CONF_TEXT_TYPE = 'text'
|
||||
|
||||
# List from https://tinyurl.com/watson-tts-docs
|
||||
SUPPORTED_VOICES = [
|
||||
"de-DE_BirgitVoice",
|
||||
"de-DE_BirgitV2Voice",
|
||||
"de-DE_DieterVoice",
|
||||
"de-DE_DieterV2Voice",
|
||||
"en-GB_KateVoice",
|
||||
"en-US_AllisonVoice",
|
||||
"en-US_AllisonV2Voice",
|
||||
"en-US_LisaVoice",
|
||||
"en-US_LisaV2Voice",
|
||||
"en-US_MichaelVoice",
|
||||
"en-US_MichaelV2Voice",
|
||||
"es-ES_EnriqueVoice",
|
||||
"es-ES_LauraVoice",
|
||||
"es-LA_SofiaVoice",
|
||||
"es-US_SofiaVoice",
|
||||
"fr-FR_ReneeVoice",
|
||||
"it-IT_FrancescaVoice",
|
||||
"it-IT_FrancescaV2Voice",
|
||||
"ja-JP_EmiVoice",
|
||||
"pt-BR_IsabelaVoice"
|
||||
]
|
||||
|
||||
SUPPORTED_OUTPUT_FORMATS = [
|
||||
'audio/flac',
|
||||
'audio/mp3',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/ogg;codecs=opus',
|
||||
'audio/ogg;codecs=vorbis',
|
||||
'audio/wav'
|
||||
]
|
||||
|
||||
CONTENT_TYPE_EXTENSIONS = {
|
||||
'audio/flac': 'flac',
|
||||
'audio/mp3': 'mp3',
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/ogg;codecs=opus': 'ogg',
|
||||
'audio/ogg;codecs=vorbis': 'ogg',
|
||||
'audio/wav': 'wav',
|
||||
}
|
||||
|
||||
DEFAULT_VOICE = 'en-US_AllisonVoice'
|
||||
DEFAULT_OUTPUT_FORMAT = 'audio/mp3'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string,
|
||||
vol.Required(CONF_APIKEY): cv.string,
|
||||
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
|
||||
vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT):
|
||||
vol.In(SUPPORTED_OUTPUT_FORMATS),
|
||||
vol.Optional(CONF_OUTPUT_AUDIO_RATE): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def get_engine(hass, config):
|
||||
"""Set up IBM Watson TTS component."""
|
||||
from ibm_watson import TextToSpeechV1
|
||||
|
||||
service = TextToSpeechV1(
|
||||
url=config[CONF_URL],
|
||||
iam_apikey=config[CONF_APIKEY]
|
||||
)
|
||||
|
||||
supported_languages = list({s[:5] for s in SUPPORTED_VOICES})
|
||||
default_voice = config[CONF_VOICE]
|
||||
output_format = config[CONF_OUTPUT_FORMAT]
|
||||
output_audio_rate = config.get(CONF_OUTPUT_AUDIO_RATE, None)
|
||||
|
||||
return WatsonTTSProvider(
|
||||
service, supported_languages, default_voice,
|
||||
output_format, output_audio_rate)
|
||||
|
||||
|
||||
class WatsonTTSProvider(Provider):
|
||||
"""IBM Watson TTS api provider."""
|
||||
|
||||
def __init__(self,
|
||||
service,
|
||||
supported_languages,
|
||||
default_voice,
|
||||
output_format,
|
||||
output_audio_rate):
|
||||
"""Initialize Watson TTS provider."""
|
||||
self.service = service
|
||||
self.supported_langs = supported_languages
|
||||
self.default_lang = default_voice[:5]
|
||||
self.default_voice = default_voice
|
||||
self.output_format = output_format
|
||||
self.output_audio_rate = output_audio_rate
|
||||
self.name = 'Watson TTS'
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
"""Return a list of supported languages."""
|
||||
return self.supported_langs
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self.default_lang
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""Return dict include default options."""
|
||||
return {CONF_VOICE: self.default_voice}
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return [CONF_VOICE]
|
||||
|
||||
def get_tts_audio(self, message, language=None, options=None):
|
||||
"""Request TTS file from Watson TTS."""
|
||||
output_format = self.output_format
|
||||
if self.output_audio_rate:
|
||||
output_format += f";rate={self.output_audio_rate}"
|
||||
|
||||
response = self.service.synthesize(
|
||||
message, accept=output_format,
|
||||
voice=self.default_voice).get_result()
|
||||
|
||||
return (CONTENT_TYPE_EXTENSIONS[self.output_format],
|
||||
response.content)
|
Loading…
Reference in New Issue