diff --git a/.gitignore b/.gitignore index a82763e1b6d..8c4eec4d180 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,6 @@ nosetests.xml # Mr Developer .mr.developer.cfg .project -.pydevproject \ No newline at end of file +.pydevproject + +.python-version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f1c2c658bb..973415c1866 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ After you finish adding support for your device: If you've added a component: - - update the file [`domain-icon.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/http/www_static/polymer/domain-icon.html) with an icon for your domain ([pick from this list](https://www.polymer-project.org/components/core-icons/demo.html)) + - update the file [`domain-icon.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/http/www_static/polymer/domain-icon.html) with an icon for your domain ([pick from this list](https://www.polymer-project.org/0.5/components/core-elements/demo.html#core-icon)) - update the demo component with two states that it provides - Add your component to home-assistant.conf.example diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py new file mode 100644 index 00000000000..97b0ec7405a --- /dev/null +++ b/homeassistant/components/modbus.py @@ -0,0 +1,100 @@ +""" +components.modbus +~~~~~~~~~~~~~~~~~~~~~~~~~ +Modbus component, using pymodbus (python3 branch) + +typical declaration in configuration.yaml + +#Modbus TCP +modbus: + type: tcp + host: 127.0.0.1 + port: 2020 + +#Modbus RTU +modbus: + type: serial + method: rtu + port: /dev/ttyUSB0 + baudrate: 9600 + stopbits: 1 + bytesize: 8 + parity: N + +""" +import logging + +from homeassistant.const import (EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +# The domain of your component. Should be equal to the name of your component +DOMAIN = "modbus" + +# List of component names (string) your component depends upon +DEPENDENCIES = [] + +# Type of network +MEDIUM = "type" + +# if MEDIUM == "serial" +METHOD = "method" +SERIAL_PORT = "port" +BAUDRATE = "baudrate" +STOPBITS = "stopbits" +BYTESIZE = "bytesize" +PARITY = "parity" + +# if MEDIUM == "tcp" or "udp" +HOST = "host" +IP_PORT = "port" + +_LOGGER = logging.getLogger(__name__) + +NETWORK = None +TYPE = None + + +def setup(hass, config): + """ Setup Modbus component. """ + + # Modbus connection type + # pylint: disable=global-statement, import-error + global TYPE + TYPE = config[DOMAIN][MEDIUM] + + # Connect to Modbus network + # pylint: disable=global-statement, import-error + global NETWORK + + if TYPE == "serial": + from pymodbus.client.sync import ModbusSerialClient as ModbusClient + NETWORK = ModbusClient(method=config[DOMAIN][METHOD], + port=config[DOMAIN][SERIAL_PORT], + baudrate=config[DOMAIN][BAUDRATE], + stopbits=config[DOMAIN][STOPBITS], + bytesize=config[DOMAIN][BYTESIZE], + parity=config[DOMAIN][PARITY]) + elif TYPE == "tcp": + from pymodbus.client.sync import ModbusTcpClient as ModbusClient + NETWORK = ModbusClient(host=config[DOMAIN][HOST], + port=config[DOMAIN][IP_PORT]) + elif TYPE == "udp": + from pymodbus.client.sync import ModbusUdpClient as ModbusClient + NETWORK = ModbusClient(host=config[DOMAIN][HOST], + port=config[DOMAIN][IP_PORT]) + else: + return False + + def stop_modbus(event): + """ Stop Modbus service""" + NETWORK.close() + + def start_modbus(event): + """ Start Modbus service""" + NETWORK.connect() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) + + # Tells the bootstrapper that the component was succesfully initialized + return True diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py new file mode 100644 index 00000000000..90593875a54 --- /dev/null +++ b/homeassistant/components/sensor/modbus.py @@ -0,0 +1,136 @@ +""" +Support for Modbus sensors. + +Configuration: +To use the Modbus sensors you will need to add something like the following to +your config/configuration.yaml + +sensor: + platform: modbus + slave: 1 + registers: + 16: + name: My integer sensor + unit: C + 24: + bits: + 0: + name: My boolean sensor + 2: + name: My other boolean sensor + +VARIABLES: + + - "slave" = slave number (ignored and can be omitted if not serial Modbus) + - "unit" = unit to attach to value (optional, ignored for boolean sensors) + - "registers" contains a list of relevant registers to read from + it can contain a "bits" section, listing relevant bits + + - each named register will create an integer sensor + - each named bit will create a boolean sensor +""" + +import logging + +import homeassistant.components.modbus as modbus +from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + TEMP_CELCIUS, TEMP_FAHRENHEIT, + STATE_ON, STATE_OFF) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Read config and create Modbus devices """ + sensors = [] + slave = config.get("slave", None) + if modbus.TYPE == "serial" and not slave: + _LOGGER.error("No slave number provided for serial Modbus") + return False + registers = config.get("registers") + for regnum, register in registers.items(): + if register.get("name"): + sensors.append(ModbusSensor(register.get("name"), + slave, + regnum, + None, + register.get("unit"))) + if register.get("bits"): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + sensors.append(ModbusSensor(bit.get("name"), + slave, + regnum, + bitnum)) + add_devices(sensors) + + +class ModbusSensor(Entity): + # pylint: disable=too-many-arguments + """ Represents a Modbus Sensor """ + + def __init__(self, name, slave, register, bit=None, unit=None): + self._name = name + self.slave = int(slave) if slave else 1 + self.register = int(register) + self.bit = int(bit) if bit else None + self._value = None + self._unit = unit + + def __str__(self): + return "%s: %s" % (self.name, self.state) + + @property + def should_poll(self): + """ We should poll, because slaves are not allowed to + initiate communication on Modbus networks""" + return True + + @property + def unique_id(self): + """ Returns a unique id. """ + return "MODBUS-SENSOR-{}-{}-{}".format(self.slave, + self.register, + self.bit) + + @property + def state(self): + """ Returns the state of the sensor. """ + if self.bit: + return STATE_ON if self._value else STATE_OFF + else: + return self._value + + @property + def name(self): + """ Get the name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + if self._unit == "C": + return TEMP_CELCIUS + elif self._unit == "F": + return TEMP_FAHRENHEIT + else: + return self._unit + + @property + def state_attributes(self): + attr = super().state_attributes + return attr + + def update(self): + result = modbus.NETWORK.read_holding_registers(unit=self.slave, + address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + if self.bit: + self._value = val & (0x0001 << self.bit) + else: + self._value = val diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py new file mode 100644 index 00000000000..7e5e039337f --- /dev/null +++ b/homeassistant/components/switch/modbus.py @@ -0,0 +1,121 @@ +""" +Support for Modbus switches. + +Configuration: +To use the Modbus switches you will need to add something like the following to +your config/configuration.yaml + +sensor: + platform: modbus + slave: 1 + registers: + 24: + bits: + 0: + name: My switch + 2: + name: My other switch + +VARIABLES: + + - "slave" = slave number (ignored and can be omitted if not serial Modbus) + - "registers" contains a list of relevant registers to read from + - it must contain a "bits" section, listing relevant bits + + - each named bit will create a switch +""" + +import logging + +import homeassistant.components.modbus as modbus +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Read config and create Modbus devices """ + switches = [] + slave = config.get("slave", None) + if modbus.TYPE == "serial" and not slave: + _LOGGER.error("No slave number provided for serial Modbus") + return False + registers = config.get("registers") + for regnum, register in registers.items(): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + switches.append(ModbusSwitch(bit.get("name"), + slave, + regnum, + bitnum)) + add_devices(switches) + + +class ModbusSwitch(ToggleEntity): + """ Represents a Modbus Switch """ + + def __init__(self, name, slave, register, bit): + self._name = name + self.slave = int(slave) if slave else 1 + self.register = int(register) + self.bit = int(bit) + self._is_on = None + self.register_value = None + + def __str__(self): + return "%s: %s" % (self.name, self.state) + + @property + def should_poll(self): + """ We should poll, because slaves are not allowed to + initiate communication on Modbus networks""" + return True + + @property + def unique_id(self): + """ Returns a unique id. """ + return "MODBUS-SWITCH-{}-{}-{}".format(self.slave, + self.register, + self.bit) + + @property + def is_on(self): + """ Returns True if switch is on. """ + return self._is_on + + @property + def name(self): + """ Get the name of the switch. """ + return self._name + + @property + def state_attributes(self): + attr = super().state_attributes + return attr + + def turn_on(self, **kwargs): + if self.register_value is None: + self.update() + val = self.register_value | (0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) + + def turn_off(self, **kwargs): + if self.register_value is None: + self.update() + val = self.register_value & ~(0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) + + def update(self): + result = modbus.NETWORK.read_holding_registers(unit=self.slave, + address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + self.register_value = val + self._is_on = (val & (0x0001 << self.bit) > 0)