diff --git a/.coveragerc b/.coveragerc index 4957221dd94..f3c7d950965 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,9 @@ omit = homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py + homeassistant/components/insteon_local.py + homeassistant/components/*/insteon_local.py + homeassistant/components/ios.py homeassistant/components/*/ios.py diff --git a/homeassistant/components/frontend/www_static/images/config_insteon.png b/homeassistant/components/frontend/www_static/images/config_insteon.png new file mode 100644 index 00000000000..0039cf3d160 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_insteon.png differ diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local.py new file mode 100644 index 00000000000..7b35b45293c --- /dev/null +++ b/homeassistant/components/insteon_local.py @@ -0,0 +1,74 @@ +""" +Local Support for Insteon. + +Based on the insteonlocal library +https://github.com/phareous/insteonlocal + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/insteon_local/ +""" +import logging +import voluptuous as vol +import requests +from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_HOST, + CONF_PORT, CONF_TIMEOUT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['insteonlocal==0.39'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'insteon_local' + +DEFAULT_PORT = 25105 + +DEFAULT_TIMEOUT = 10 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup Insteon Hub component. + + This will automatically import associated lights. + """ + from insteonlocal.Hub import Hub + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + timeout = conf.get(CONF_TIMEOUT) + + try: + insteonhub = Hub(host, username, password, port, timeout, _LOGGER) + # check for successful connection + insteonhub.get_buffer_status() + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Error on insteon_local." + "Could not connect. Check config", exc_info=True) + return False + except requests.exceptions.ConnectionError: + _LOGGER.error("Error on insteon_local. Could not connect." + "Check config", exc_info=True) + return False + except requests.exceptions.RequestException: + if insteonhub.http_code == 401: + _LOGGER.error("Bad user/pass for insteon_local hub") + return False + else: + _LOGGER.error("Error on insteon_local hub check", exc_info=True) + return False + + hass.data['insteon_local'] = insteonhub + + return True diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py new file mode 100644 index 00000000000..9c40ec9c4f7 --- /dev/null +++ b/homeassistant/components/light/insteon_local.py @@ -0,0 +1,191 @@ +""" +Support for Insteon dimmers via local hub control. + +Based on the insteonlocal library +https://github.com/phareous/insteonlocal + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.insteon_local/ + +-- +Example platform config +-- + +insteon_local: + host: YOUR HUB IP + username: YOUR HUB USERNAME + password: YOUR HUB PASSWORD + timeout: 10 + port: 25105 + +""" +import json +import logging +import os +from datetime import timedelta +from homeassistant.components.light import (ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, Light) +from homeassistant.loader import get_component +import homeassistant.util as util + +INSTEON_LOCAL_LIGHTS_CONF = 'insteon_local_lights.conf' + +DEPENDENCIES = ['insteon_local'] + +SUPPORT_INSTEON_LOCAL = SUPPORT_BRIGHTNESS + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) + +DOMAIN = "light" + +_LOGGER = logging.getLogger(__name__) +_CONFIGURING = {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Insteon local light platform.""" + insteonhub = hass.data['insteon_local'] + + conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) + if len(conf_lights): + for device_id in conf_lights: + setup_light(device_id, conf_lights[device_id], insteonhub, hass, + add_devices) + + linked = insteonhub.get_linked() + + for device_id in linked: + if (linked[device_id]['cat_type'] == 'dimmer' and + device_id not in conf_lights): + request_configuration(device_id, + insteonhub, + linked[device_id]['model_name'] + ' ' + + linked[device_id]['sku'], hass, add_devices) + + +def request_configuration(device_id, insteonhub, model, hass, + add_devices_callback): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if device_id in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[device_id], 'Failed to register, please try again.') + + return + + def insteon_light_config_callback(data): + """The actions to do when our configuration callback is called.""" + setup_light(device_id, data.get('name'), insteonhub, hass, + add_devices_callback) + + _CONFIGURING[device_id] = configurator.request_config( + hass, 'Insteon ' + model + ' addr: ' + device_id, + insteon_light_config_callback, + description=('Enter a name for ' + model + ' addr: ' + device_id), + entity_picture='/static/images/config_insteon.png', + submit_caption='Confirm', + fields=[{'id': 'name', 'name': 'Name', 'type': ''}] + ) + + +def setup_light(device_id, name, insteonhub, hass, add_devices_callback): + """Setup light.""" + if device_id in _CONFIGURING: + request_id = _CONFIGURING.pop(device_id) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Device configuration done!') + + conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) + if device_id not in conf_lights: + conf_lights[device_id] = name + + if not config_from_file( + hass.config.path(INSTEON_LOCAL_LIGHTS_CONF), + conf_lights): + _LOGGER.error('failed to save config file') + + device = insteonhub.dimmer(device_id) + add_devices_callback([InsteonLocalDimmerDevice(device, name)]) + + +def config_from_file(filename, config=None): + """Small configuration file management function.""" + if config: + # We're writing configuration + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +class InsteonLocalDimmerDevice(Light): + """An abstract Class for an Insteon node.""" + + def __init__(self, node, name): + """Initialize the device.""" + self.node = node + self.node.deviceName = name + self._value = 0 + + @property + def name(self): + """Return the the name of the node.""" + return self.node.deviceName + + @property + def unique_id(self): + """Return the ID of this insteon node.""" + return 'insteon_local_' + self.node.device_id + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._value + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Update state of the light.""" + resp = self.node.status(0) + if 'cmd2' in resp: + self._value = int(resp['cmd2'], 16) + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._value != 0 + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_INSTEON_LOCAL + + def turn_on(self, **kwargs): + """Turn device on.""" + brightness = 100 + if ATTR_BRIGHTNESS in kwargs: + brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100 + + self.node.on(brightness) + + def turn_off(self, **kwargs): + """Turn device off.""" + self.node.off() diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py new file mode 100644 index 00000000000..cc6a732bb7f --- /dev/null +++ b/homeassistant/components/switch/insteon_local.py @@ -0,0 +1,176 @@ +""" +Support for Insteon switch devices via local hub support. + +Based on the insteonlocal library +https://github.com/phareous/insteonlocal + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.insteon_local/ + +-- +Example platform config +-- + +insteon_local: + host: YOUR HUB IP + username: YOUR HUB USERNAME + password: YOUR HUB PASSWORD + timeout: 10 + port: 25105 +""" +import json +import logging +import os +from datetime import timedelta +from homeassistant.components.switch import SwitchDevice +from homeassistant.loader import get_component +import homeassistant.util as util + +INSTEON_LOCAL_SWITCH_CONF = 'insteon_local_switch.conf' + +DEPENDENCIES = ['insteon_local'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) + +DOMAIN = "switch" + +_LOGGER = logging.getLogger(__name__) +_CONFIGURING = {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Insteon local switch platform.""" + insteonhub = hass.data['insteon_local'] + + conf_switches = config_from_file(hass.config.path( + INSTEON_LOCAL_SWITCH_CONF)) + if len(conf_switches): + for device_id in conf_switches: + setup_switch(device_id, conf_switches[device_id], insteonhub, + hass, add_devices) + + linked = insteonhub.get_inked() + + for device_id in linked: + if linked[device_id]['cat_type'] == 'switch'\ + and device_id not in conf_switches: + request_configuration(device_id, insteonhub, + linked[device_id]['model_name'] + ' ' + + linked[device_id]['sku'], hass, add_devices) + + +def request_configuration(device_id, insteonhub, model, hass, + add_devices_callback): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if device_id in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[device_id], 'Failed to register, please try again.') + + return + + def insteon_switch_config_callback(data): + """The actions to do when our configuration callback is called.""" + setup_switch(device_id, data.get('name'), insteonhub, hass, + add_devices_callback) + + _CONFIGURING[device_id] = configurator.request_config( + hass, 'Insteon Switch ' + model + ' addr: ' + device_id, + insteon_switch_config_callback, + description=('Enter a name for ' + model + ' addr: ' + device_id), + entity_picture='/static/images/config_insteon.png', + submit_caption='Confirm', + fields=[{'id': 'name', 'name': 'Name', 'type': ''}] + ) + + +def setup_switch(device_id, name, insteonhub, hass, add_devices_callback): + """Setup switch.""" + if device_id in _CONFIGURING: + request_id = _CONFIGURING.pop(device_id) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Device configuration done!') + + conf_switch = config_from_file(hass.config.path(INSTEON_LOCAL_SWITCH_CONF)) + if device_id not in conf_switch: + conf_switch[device_id] = name + + if not config_from_file( + hass.config.path(INSTEON_LOCAL_SWITCH_CONF), conf_switch): + _LOGGER.error('failed to save config file') + + device = insteonhub.switch(device_id) + add_devices_callback([InsteonLocalSwitchDevice(device, name)]) + + +def config_from_file(filename, config=None): + """Small configuration file management function.""" + if config: + # We're writing configuration + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +class InsteonLocalSwitchDevice(SwitchDevice): + """An abstract Class for an Insteon node.""" + + def __init__(self, node, name): + """Initialize the device.""" + self.node = node + self.node.deviceName = name + self._state = False + + @property + def name(self): + """Return the the name of the node.""" + return self.node.deviceName + + @property + def unique_id(self): + """Return the ID of this insteon node.""" + return 'insteon_local_' + self.node.device_id + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Get the updated status of the switch.""" + resp = self.node.status(0) + if 'cmd2' in resp: + self._state = int(resp['cmd2'], 16) > 0 + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn device on.""" + self.node.on() + self._state = True + + def turn_off(self, **kwargs): + """Turn device off.""" + self.node.off() + self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index e17c6b2c8a8..a2412504bdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -271,6 +271,9 @@ influxdb==3.0.0 # homeassistant.components.insteon_hub insteon_hub==0.4.5 +# homeassistant.components.insteon_local +insteonlocal==0.39 + # homeassistant.components.media_player.kodi jsonrpc-async==0.1