diff --git a/.coveragerc b/.coveragerc index 45b67e14b84..756c82f755e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,9 @@ omit = homeassistant/external/* # omit pieces of code that rely on external devices being present + homeassistant/components/arduino.py + homeassistant/components/*/arduino.py + homeassistant/components/wink.py homeassistant/components/*/wink.py diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py new file mode 100644 index 00000000000..e7131f9c9e0 --- /dev/null +++ b/homeassistant/components/arduino.py @@ -0,0 +1,134 @@ +""" +components.arduino +~~~~~~~~~~~~~~~~~~ +Arduino component that connects to a directly attached Arduino board which +runs with the Firmata firmware. + +Configuration: + +To use the Arduino board you will need to add something like the following +to your config/configuration.yaml + +arduino: + port: /dev/ttyACM0 + +Variables: + +port +*Required +The port where is your board connected to your Home Assistant system. +If you are using an original Arduino the port will be named ttyACM*. The exact +number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/ +'journalctl -f' output. Keep in mind that Arduino clones are often using a +different name for the port (e.g. '/dev/ttyUSB*'). + +A word of caution: The Arduino is not storing states. This means that with +every initialization the pins are set to off/low. +""" +import logging + +from PyMata.pymata import PyMata +import serial + +from homeassistant.helpers import validate_config +from homeassistant.const import (EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +DOMAIN = "arduino" +DEPENDENCIES = [] +BOARD = None +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ Setup the Arduino component. """ + + if not validate_config(config, + {DOMAIN: ['port']}, + _LOGGER): + return False + + # pylint: disable=global-statement + global BOARD + try: + BOARD = ArduinoBoard(config[DOMAIN]['port']) + except (serial.serialutil.SerialException, FileNotFoundError): + _LOGGER.exception("Your port is not accessible.") + return False + + if BOARD.get_firmata()[1] <= 2: + _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.") + return False + + def stop_arduino(event): + """ Stop the Arduino service. """ + BOARD.disconnect() + + def start_arduino(event): + """ Start the Arduino service. """ + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino) + + return True + + +class ArduinoBoard(object): + """ Represents an Arduino board. """ + + def __init__(self, port): + self._port = port + self._board = PyMata(self._port, verbose=False) + + def set_mode(self, pin, direction, mode): + """ Sets the mode and the direction of a given pin. """ + if mode == 'analog' and direction == 'in': + self._board.set_pin_mode(pin, + self._board.INPUT, + self._board.ANALOG) + elif mode == 'analog' and direction == 'out': + self._board.set_pin_mode(pin, + self._board.OUTPUT, + self._board.ANALOG) + elif mode == 'digital' and direction == 'in': + self._board.set_pin_mode(pin, + self._board.OUTPUT, + self._board.DIGITAL) + elif mode == 'digital' and direction == 'out': + self._board.set_pin_mode(pin, + self._board.OUTPUT, + self._board.DIGITAL) + elif mode == 'pwm': + self._board.set_pin_mode(pin, + self._board.OUTPUT, + self._board.PWM) + + def get_analog_inputs(self): + """ Get the values from the pins. """ + self._board.capability_query() + return self._board.get_analog_response_table() + + def set_digital_out_high(self, pin): + """ Sets a given digital pin to high. """ + self._board.digital_write(pin, 1) + + def set_digital_out_low(self, pin): + """ Sets a given digital pin to low. """ + self._board.digital_write(pin, 0) + + def get_digital_in(self, pin): + """ Gets the value from a given digital pin. """ + self._board.digital_read(pin) + + def get_analog_in(self, pin): + """ Gets the value from a given analog pin. """ + self._board.analog_read(pin) + + def get_firmata(self): + """ Return the version of the Firmata firmware. """ + return self._board.get_firmata_version() + + def disconnect(self): + """ Disconnects the board and closes the serial connection. """ + self._board.reset() + self._board.close() diff --git a/homeassistant/components/sensor/arduino.py b/homeassistant/components/sensor/arduino.py new file mode 100644 index 00000000000..4210a064d13 --- /dev/null +++ b/homeassistant/components/sensor/arduino.py @@ -0,0 +1,87 @@ +""" +homeassistant.components.sensor.arduino +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for getting information from Arduino pins. Only analog pins are +supported. + +Configuration: + +sensor: + platform: arduino + pins: + 7: + name: Door switch + type: analog + 0: + name: Brightness + type: analog + +Variables: + +pins +*Required +An array specifying the digital pins to use on the Arduino board. + +These are the variables for the pins array: + +name +*Required +The name for the pin that will be used in the frontend. + +type +*Required +The type of the pin: 'analog'. +""" +import logging + +import homeassistant.components.arduino as arduino +from homeassistant.helpers.entity import Entity +from homeassistant.const import DEVICE_DEFAULT_NAME + +DEPENDENCIES = ['arduino'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Arduino platform. """ + + # Verify that Arduino board is present + if arduino.BOARD is None: + _LOGGER.error('A connection has not been made to the Arduino board.') + return False + + sensors = [] + pins = config.get('pins') + for pinnum, pin in pins.items(): + if pin.get('name'): + sensors.append(ArduinoSensor(pin.get('name'), + pinnum, + 'analog')) + add_devices(sensors) + + +class ArduinoSensor(Entity): + """ Represents an Arduino Sensor. """ + def __init__(self, name, pin, pin_type): + self._pin = pin + self._name = name or DEVICE_DEFAULT_NAME + self.pin_type = pin_type + self.direction = 'in' + self._value = None + + arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type) + + @property + def state(self): + """ Returns the state of the sensor. """ + return self._value + + @property + def name(self): + """ Get the name of the sensor. """ + return self._name + + def update(self): + """ Get the latest value from the pin. """ + self._value = arduino.BOARD.get_analog_inputs()[self._pin][1] diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/switch/arduino.py new file mode 100644 index 00000000000..367e7378b27 --- /dev/null +++ b/homeassistant/components/switch/arduino.py @@ -0,0 +1,93 @@ +""" +homeassistant.components.switch.arduino +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for switching Arduino pins on and off. So fare only digital pins are +supported. + +Configuration: + +switch: + platform: arduino + pins: + 11: + name: Fan Office + type: digital + 12: + name: Light Desk + type: digital + +Variables: + +pins +*Required +An array specifying the digital pins to use on the Arduino board. + +These are the variables for the pins array: + +name +*Required +The name for the pin that will be used in the frontend. + +type +*Required +The type of the pin: 'digital'. +""" +import logging + +import homeassistant.components.arduino as arduino +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import DEVICE_DEFAULT_NAME + +DEPENDENCIES = ['arduino'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Arduino platform. """ + + # Verify that Arduino board is present + if arduino.BOARD is None: + _LOGGER.error('A connection has not been made to the Arduino board.') + return False + + switches = [] + pins = config.get('pins') + for pinnum, pin in pins.items(): + if pin.get('name'): + switches.append(ArduinoSwitch(pin.get('name'), + pinnum, + pin.get('type'))) + add_devices(switches) + + +class ArduinoSwitch(SwitchDevice): + """ Represents an Arduino Switch. """ + def __init__(self, name, pin, pin_type): + self._pin = pin + self._name = name or DEVICE_DEFAULT_NAME + self.pin_type = pin_type + self.direction = 'out' + self._state = False + + arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type) + + @property + def name(self): + """ Get the name of the pin. """ + return self._name + + @property + def is_on(self): + """ Returns True if pin is high/on. """ + return self._state + + def turn_on(self): + """ Turns the pin to high/on. """ + self._state = True + arduino.BOARD.set_digital_out_high(self._pin) + + def turn_off(self): + """ Turns the pin to low/off. """ + self._state = False + arduino.BOARD.set_digital_out_low(self._pin) diff --git a/requirements.txt b/requirements.txt index 2684855be54..49d6a7033dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -73,3 +73,6 @@ jsonrpc-requests>=0.1 # Forecast.io Bindings (sensor.forecast) python-forecastio>=1.3.3 + +# Firmata Bindings (*.arduino) +PyMata==2.07a