From 92411cdc1898aa9e7eb403dbcd7102a08cfedc76 Mon Sep 17 00:00:00 2001 From: florincosta Date: Fri, 5 May 2017 10:02:47 +0300 Subject: [PATCH] Add new raspihats component (#7392) * Add new raspihats component * added raspihats to COMMENT_REQUIREMENTS in gen_requirements_all.py * disabled pylint import errors * using hass.data for storing i2c-hats manager --- .coveragerc | 2 + homeassistant/components/raspihats.py | 249 ++++++++++++++++++++++++++ requirements_all.txt | 3 + script/gen_requirements_all.py | 1 + 4 files changed, 255 insertions(+) create mode 100644 homeassistant/components/raspihats.py diff --git a/.coveragerc b/.coveragerc index 9656508982a..bbc40156cb5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -83,6 +83,8 @@ omit = homeassistant/components/qwikswitch.py homeassistant/components/*/qwikswitch.py + homeassistant/components/raspihats.py + homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py new file mode 100644 index 00000000000..3ab433f4b91 --- /dev/null +++ b/homeassistant/components/raspihats.py @@ -0,0 +1,249 @@ +""" +Support for controlling raspihats boards. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/raspihats/ +""" +import logging +import threading +import time + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +) + +REQUIREMENTS = ['raspihats==2.2.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'raspihats' + +CONF_I2C_HATS = 'i2c_hats' +CONF_BOARD = 'board' +CONF_ADDRESS = 'address' +CONF_CHANNELS = 'channels' +CONF_INDEX = 'index' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_INITIAL_STATE = 'initial_state' + +I2C_HAT_NAMES = [ + 'Di16', 'Rly10', 'Di6Rly6', + 'DI16ac', 'DQ10rly', 'DQ16oc', 'DI6acDQ6rly' +] + +I2C_HATS_MANAGER = 'I2CH_MNG' + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the raspihats component.""" + hass.data[I2C_HATS_MANAGER] = I2CHatsManager() + + def start_i2c_hats_keep_alive(event): + """Start I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].start_keep_alive() + + def stop_i2c_hats_keep_alive(event): + """Stop I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].stop_keep_alive() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) + return True + + +def log_message(source, *parts): + """Build log message.""" + message = source.__class__.__name__ + for part in parts: + message += ": " + str(part) + return message + + +class I2CHatsException(Exception): + """I2C-HATs exception.""" + + +class I2CHatsDIScanner(object): + """Scan Digital Inputs and fire callbacks.""" + + _DIGITAL_INPUTS = "di" + _OLD_VALUE = "old_value" + _CALLBACKS = "callbacks" + + def setup(self, i2c_hat): + """Setup I2C-HAT instance for digital inputs scanner.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + old_value = None + # add old value attribute + setattr(digital_inputs, self._OLD_VALUE, old_value) + # add callbacks dict attribute {channel: callback} + setattr(digital_inputs, self._CALLBACKS, {}) + + def register_callback(self, i2c_hat, channel, callback): + """Register edge callback.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + callbacks[channel] = callback + setattr(digital_inputs, self._CALLBACKS, callbacks) + + def scan(self, i2c_hat): + """Scan I2C-HATs digital inputs and fire callbacks.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + old_value = getattr(digital_inputs, self._OLD_VALUE) + value = digital_inputs.value # i2c data transfer + if old_value is not None and value != old_value: + for channel in range(0, len(digital_inputs.channels)): + state = (value >> channel) & 0x01 + old_state = (old_value >> channel) & 0x01 + if state != old_state: + callback = callbacks.get(channel, None) + if callback is not None: + callback(state) + setattr(digital_inputs, self._OLD_VALUE, value) + + +class I2CHatsManager(threading.Thread): + """Manages all I2C-HATs instances.""" + + _EXCEPTION = "exception" + _CALLBACKS = "callbacks" + + def __init__(self): + """Init I2C-HATs Manager.""" + threading.Thread.__init__(self) + self._lock = threading.Lock() + self._i2c_hats = {} + self._run = False + self._di_scanner = I2CHatsDIScanner() + + def register_board(self, board, address): + """Register I2C-HAT.""" + with self._lock: + i2c_hat = self._i2c_hats.get(address) + if i2c_hat is None: + # pylint: disable=import-error + import raspihats.i2c_hats as module + constructor = getattr(module, board) + i2c_hat = constructor(address) + setattr(i2c_hat, self._CALLBACKS, {}) + + # Setting exception attribute will trigger online callbacks + # when keep alive thread starts. + setattr(i2c_hat, self._EXCEPTION, None) + + self._di_scanner.setup(i2c_hat) + self._i2c_hats[address] = i2c_hat + status_word = i2c_hat.status # read status_word to reset bits + _LOGGER.info( + log_message(self, i2c_hat, "registered", status_word) + ) + + def run(self): + """Keep alive for I2C-HATs.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + _LOGGER.info( + log_message(self, "starting") + ) + while self._run: + with self._lock: + for i2c_hat in list(self._i2c_hats.values()): + try: + self._di_scanner.scan(i2c_hat) + self._read_status(i2c_hat) + + if hasattr(i2c_hat, self._EXCEPTION): + if getattr(i2c_hat, self._EXCEPTION) is not None: + _LOGGER.warning( + log_message(self, i2c_hat, "online again") + ) + delattr(i2c_hat, self._EXCEPTION) + # trigger online callbacks + callbacks = getattr(i2c_hat, self._CALLBACKS) + for callback in list(callbacks.values()): + callback() + except ResponseException as ex: + if not hasattr(i2c_hat, self._EXCEPTION): + _LOGGER.error( + log_message(self, i2c_hat, ex) + ) + setattr(i2c_hat, self._EXCEPTION, ex) + time.sleep(0.05) + _LOGGER.info( + log_message(self, "exiting") + ) + + def _read_status(self, i2c_hat): + """Read I2C-HATs status.""" + status_word = i2c_hat.status + if status_word.value != 0x00: + _LOGGER.error( + log_message(self, i2c_hat, status_word) + ) + + def start_keep_alive(self): + """Start keep alive mechanism.""" + self._run = True + threading.Thread.start(self) + + def stop_keep_alive(self): + """Stop keep alive mechanism.""" + self._run = False + self.join() + + def register_di_callback(self, address, channel, callback): + """Register I2C-HAT digital input edge callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + self._di_scanner.register_callback(i2c_hat, channel, callback) + + def register_online_callback(self, address, channel, callback): + """Register I2C-HAT online callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + callbacks = getattr(i2c_hat, self._CALLBACKS) + callbacks[channel] = callback + setattr(i2c_hat, self._CALLBACKS, callbacks) + + def read_di(self, address, channel): + """Read a value from a I2C-HAT digital input.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + value = i2c_hat.di.value + return (value >> channel) & 0x01 + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def write_dq(self, address, channel, value): + """Write a value to a I2C-HAT digital output.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + i2c_hat.dq.channels[channel] = value + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def read_dq(self, address, channel): + """Read a value from a I2C-HAT digital output.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + return i2c_hat.dq.channels[channel] + except ResponseException as ex: + raise I2CHatsException(str(ex)) diff --git a/requirements_all.txt b/requirements_all.txt index 4f0edd805e1..9e917905b4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -721,6 +721,9 @@ qnapstats==0.2.4 # homeassistant.components.climate.radiotherm radiotherm==1.2 +# homeassistant.components.raspihats +# raspihats==2.2.1 + # homeassistant.components.rflink rflink==0.0.31 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4d08ff349a0..b6424fc5fdd 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,6 +8,7 @@ import sys COMMENT_REQUIREMENTS = ( 'RPi.GPIO', + 'raspihats', 'rpi-rf', 'Adafruit_Python_DHT', 'Adafruit_BBIO',