Factored Client code out & implemented LIRC network listener
This commit is contained in:
parent
20f43f4294
commit
1a746d1438
|
@ -31,17 +31,13 @@ dimmer:
|
||||||
# Weather prediction
|
# Weather prediction
|
||||||
# - platform: yr
|
# - platform: yr
|
||||||
|
|
||||||
# Text to speech
|
|
||||||
#tts:
|
|
||||||
## - platform: google_translate
|
|
||||||
# language: 'de'
|
|
||||||
# base_url: http://192.168.178.73:8123
|
|
||||||
tts:
|
tts:
|
||||||
- platform: watson_tts
|
- platform: watson_tts
|
||||||
watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7
|
watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7
|
||||||
watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api
|
watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api
|
||||||
voice: de-DE_BirgitVoice
|
voice: de-DE_BirgitVoice
|
||||||
output_format: audio/flac;rate=44100
|
output_format: audio/flac
|
||||||
|
output_audio_rate: 44100
|
||||||
|
|
||||||
knx:
|
knx:
|
||||||
rate_limit: 20
|
rate_limit: 20
|
||||||
|
@ -57,6 +53,13 @@ fhem:
|
||||||
cul_device_name: CUL_HM
|
cul_device_name: CUL_HM
|
||||||
|
|
||||||
|
|
||||||
|
lirc_network:
|
||||||
|
- host: kitchenpi
|
||||||
|
port: 2222
|
||||||
|
- host: bedroompi
|
||||||
|
port: 2222
|
||||||
|
|
||||||
|
|
||||||
media_player:
|
media_player:
|
||||||
- platform: squeezebox
|
- platform: squeezebox
|
||||||
host: server
|
host: server
|
||||||
|
@ -66,7 +69,6 @@ media_player:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
group: !include groups.yaml
|
group: !include groups.yaml
|
||||||
automation: !include automations.yaml
|
automation: !include automations.yaml
|
||||||
script: !include scripts.yaml
|
script: !include scripts.yaml
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import asyncio
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import homeassistant.helpers.config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONF_CUL_DEVICE_NAME = 'cul_device_name'
|
CONF_CUL_DEVICE_NAME = 'cul_device_name'
|
||||||
|
@ -23,28 +23,20 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
connection = FhemConnection(hass, config)
|
connection = FhemConnection(hass, config[DOMAIN])
|
||||||
hass.data[DATA_FHEM] = connection
|
hass.data[DATA_FHEM] = connection
|
||||||
await connection.start()
|
await connection.start()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FhemConnection:
|
class FhemConnection(ReconnectingClient):
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
self.hass = hass
|
super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "FHEM",
|
||||||
self._host = config[DOMAIN][CONF_HOST]
|
receive_line_callback=self._process_line,
|
||||||
self._port = config[DOMAIN][CONF_PORT]
|
connection_status_changed_callback=self._update_all_devices)
|
||||||
self._cul_device_name = config[DOMAIN][CONF_CUL_DEVICE_NAME]
|
self._cul_device_name = config[CONF_CUL_DEVICE_NAME]
|
||||||
|
|
||||||
self.connected = False
|
|
||||||
self.reconnect_time_start = 1
|
|
||||||
self.reconnect_time_max = 60
|
|
||||||
self.reconnect_time = self.reconnect_time_start
|
|
||||||
self._devices = defaultdict(list)
|
self._devices = defaultdict(list)
|
||||||
self._run = False
|
|
||||||
self._writer = None
|
|
||||||
self._connection_last_state = 'UNKNOWN'
|
|
||||||
|
|
||||||
def register_device(self, id, d):
|
def register_device(self, id, d):
|
||||||
self._devices[id].append(d)
|
self._devices[id].append(d)
|
||||||
|
@ -53,56 +45,11 @@ class FhemConnection:
|
||||||
"displayattr .*\n".encode(),
|
"displayattr .*\n".encode(),
|
||||||
])
|
])
|
||||||
|
|
||||||
async def _update_all_devices(self):
|
async def _update_all_devices(self, _):
|
||||||
for device_list in self._devices.values():
|
for device_list in self._devices.values():
|
||||||
for device in device_list:
|
for device in device_list:
|
||||||
await device.async_update_ha_state()
|
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):
|
async def _process_line(self, line):
|
||||||
if line.startswith(self._cul_device_name + " "): # Status update message
|
if line.startswith(self._cul_device_name + " "): # Status update message
|
||||||
_, device_name, command = line.split(" ", 2)
|
_, device_name, command = line.split(" ", 2)
|
||||||
|
@ -127,7 +74,7 @@ class FhemConnection:
|
||||||
:param arguments: string or list of strings containing command parameters
|
:param arguments: string or list of strings containing command parameters
|
||||||
"""
|
"""
|
||||||
arguments = " ".join([str(a) for a in arguments])
|
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):
|
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