diff --git a/.coveragerc b/.coveragerc index 26b6413b4c0d1b7c1d604b1529b4e81145ec1f93..fe4796ae53e0051597d70b3227cd97bd55eef76b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -66,6 +66,7 @@ omit = homeassistant/components/*/scsgate.py homeassistant/components/upnp.py + homeassistant/components/zeroconf.py homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py @@ -175,6 +176,7 @@ omit = homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py + homeassistant/components/*/thinkingcleaner.py [report] diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 1866e972e280002b01377308f87363452747f168..beb9e4a7214e70c63b3c583f9121e4acd102081d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -94,7 +94,7 @@ def setup(hass, config): yaml_path = hass.config.path(YAML_DEVICES) conf = config.get(DOMAIN, {}) - if isinstance(conf, list): + if isinstance(conf, list) and len(conf) > 0: conf = conf[0] consider_home = timedelta( seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int, diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f03fdf35929dd7b25965e8d2b9c7e375dceba207..900d826e61a643bfeaf8bc7ade0ce0f5da7dcccf 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -15,7 +15,7 @@ from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED) DOMAIN = "discovery" -REQUIREMENTS = ['netdisco==0.6.2'] +REQUIREMENTS = ['netdisco==0.6.4'] SCAN_INTERVAL = 300 # seconds diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py new file mode 100644 index 0000000000000000000000000000000000000000..1ba8593650ea2d47809bc1c24ecf19686b8ba1be --- /dev/null +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -0,0 +1,114 @@ +"""Support for ThinkingCleaner.""" +import logging +from datetime import timedelta + +import homeassistant.util as util +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/TheRealLink/pythinkingcleaner' + '/archive/v0.0.2.zip' + '#pythinkingcleaner==0.0.2'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) + +SENSOR_TYPES = { + 'battery': ['Battery', '%', 'mdi:battery'], + 'state': ['State', None, None], + 'capacity': ['Capacity', None, None], +} + +STATES = { + 'st_base': 'On homebase: Not Charging', + 'st_base_recon': 'On homebase: Reconditioning Charging', + 'st_base_full': 'On homebase: Full Charging', + 'st_base_trickle': 'On homebase: Trickle Charging', + 'st_base_wait': 'On homebase: Waiting', + 'st_plug': 'Plugged in: Not Charging', + 'st_plug_recon': 'Plugged in: Reconditioning Charging', + 'st_plug_full': 'Plugged in: Full Charging', + 'st_plug_trickle': 'Plugged in: Trickle Charging', + 'st_plug_wait': 'Plugged in: Waiting', + 'st_stopped': 'Stopped', + 'st_clean': 'Cleaning', + 'st_cleanstop': 'Stopped with cleaning', + 'st_clean_spot': 'Spot cleaning', + 'st_clean_max': 'Max cleaning', + 'st_delayed': 'Delayed cleaning will start soon', + 'st_dock': 'Searching Homebase', + 'st_pickup': 'Roomba picked up', + 'st_remote': 'Remote control driving', + 'st_wait': 'Waiting for command', + 'st_off': 'Off', + 'st_error': 'Error', + 'st_locate': 'Find me!', + 'st_unknown': 'Unknown state', +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ThinkingCleaner platform.""" + from pythinkingcleaner import Discovery + + discovery = Discovery() + devices = discovery.discover() + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update_devices(): + """Update all devices.""" + for device_object in devices: + device_object.update() + + dev = [] + for device in devices: + for type_name in SENSOR_TYPES.keys(): + dev.append(ThinkingCleanerSensor(device, type_name, + update_devices)) + + add_devices(dev) + + +class ThinkingCleanerSensor(Entity): + """ThinkingCleaner Sensor.""" + + def __init__(self, tc_object, sensor_type, update_devices): + """Initialize the ThinkingCleaner.""" + self.type = sensor_type + + self._tc_object = tc_object + self._update_devices = update_devices + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._tc_object.name + ' ' + SENSOR_TYPES[self.type][0] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + def update(self): + """Update the sensor.""" + self._update_devices() + + if self.type == 'battery': + self._state = self._tc_object.battery + elif self.type == 'state': + self._state = STATES[self._tc_object.status] + elif self.type == 'capacity': + self._state = self._tc_object.capacity diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/switch/thinkingcleaner.py new file mode 100644 index 0000000000000000000000000000000000000000..3bc4484db3880e5a266137f373d72f787acc6519 --- /dev/null +++ b/homeassistant/components/switch/thinkingcleaner.py @@ -0,0 +1,131 @@ +"""Support for ThinkingCleaner.""" +import time +import logging +from datetime import timedelta + +import homeassistant.util as util + +from homeassistant.const import (STATE_ON, STATE_OFF) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/TheRealLink/pythinkingcleaner' + '/archive/v0.0.2.zip' + '#pythinkingcleaner==0.0.2'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) + +MIN_TIME_TO_WAIT = timedelta(seconds=5) +MIN_TIME_TO_LOCK_UPDATE = 5 + +SWITCH_TYPES = { + 'clean': ['Clean', None, None], + 'dock': ['Dock', None, None], + 'find': ['Find', None, None], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ThinkingCleaner platform.""" + from pythinkingcleaner import Discovery + + discovery = Discovery() + devices = discovery.discover() + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update_devices(): + """Update all devices.""" + for device_object in devices: + device_object.update() + + dev = [] + for device in devices: + for type_name in SWITCH_TYPES.keys(): + dev.append(ThinkingCleanerSwitch(device, type_name, + update_devices)) + + add_devices(dev) + + +class ThinkingCleanerSwitch(ToggleEntity): + """ThinkingCleaner Switch (dock, clean, find me).""" + + def __init__(self, tc_object, switch_type, update_devices): + """Initialize the ThinkingCleaner.""" + self.type = switch_type + + self._update_devices = update_devices + self._tc_object = tc_object + self._state = \ + self._tc_object.is_cleaning if switch_type == 'clean' else False + self.lock = False + self.last_lock_time = None + self.graceful_state = False + + def lock_update(self): + """Lock the update since TC clean takes some time to update.""" + if self.is_update_locked(): + return + self.lock = True + self.last_lock_time = time.time() + + def reset_update_lock(self): + """Reset the update lock.""" + self.lock = False + self.last_lock_time = None + + def set_graceful_lock(self, state): + """Set the graceful state.""" + self.graceful_state = state + self.reset_update_lock() + self.lock_update() + + def is_update_locked(self): + """Check if the update method is locked.""" + if self.last_lock_time is None: + return False + + if time.time() - self.last_lock_time >= MIN_TIME_TO_LOCK_UPDATE: + self.last_lock_time = None + return False + + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._tc_object.name + ' ' + SWITCH_TYPES[self.type][0] + + @property + def is_on(self): + """Return true if device is on.""" + if self.type == 'clean': + return self.graceful_state \ + if self.is_update_locked() else self._tc_object.is_cleaning + + return False + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self.type == 'clean': + self.set_graceful_lock(True) + self._tc_object.start_cleaning() + elif self.type == 'dock': + self._tc_object.dock() + elif self.type == 'find': + self._tc_object.find_me() + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self.type == 'clean': + self.set_graceful_lock(False) + self._tc_object.stop_cleaning() + + def update(self): + """Update the switch state (Only for clean).""" + if self.type == 'clean' and not self.is_update_locked(): + self._tc_object.update() + self._state = STATE_ON \ + if self._tc_object.is_cleaning else STATE_OFF diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf.py new file mode 100644 index 0000000000000000000000000000000000000000..bda6dcb153ba8e3086841b0ebf58aaaeafba67d6 --- /dev/null +++ b/homeassistant/components/zeroconf.py @@ -0,0 +1,50 @@ +""" +This module exposes Home Assistant via Zeroconf. + +Zeroconf is also known as Bonjour, Avahi or Multicast DNS (mDNS). + +For more details about Zeroconf, please refer to the documentation at +https://home-assistant.io/components/zeroconf/ +""" +import logging +import socket + +from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) + +REQUIREMENTS = ["zeroconf==0.17.5"] + +DEPENDENCIES = ["api"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "zeroconf" + +ZEROCONF_TYPE = "_home-assistant._tcp.local." + + +def setup(hass, config): + """Set up Zeroconf and make Home Assistant discoverable.""" + from zeroconf import Zeroconf, ServiceInfo + + zeroconf = Zeroconf() + + zeroconf_name = "{}.{}".format(hass.config.location_name, + ZEROCONF_TYPE) + + params = {"version": __version__, "base_url": hass.config.api.base_url, + "needs_auth": (hass.config.api.api_password is not None)} + + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, + socket.inet_aton(hass.config.api.host), + hass.config.api.port, 0, 0, params) + + zeroconf.register_service(info) + + def stop_zeroconf(event): + """Stop Zeroconf.""" + zeroconf.unregister_service(info) + zeroconf.close() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index c1c678f4f4410f5e19fae17627cd1b1a0a8970fd..6b34aad3be1b58c6a60bcaf18508d0b00d2139af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -76,6 +76,10 @@ https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1 +# homeassistant.components.sensor.thinkingcleaner +# homeassistant.components.switch.thinkingcleaner +https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcleaner==0.0.2 + # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 @@ -132,7 +136,7 @@ messagebird==1.1.1 mficlient==0.3.0 # homeassistant.components.discovery -netdisco==0.6.2 +netdisco==0.6.4 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 @@ -313,3 +317,6 @@ xbee-helper==0.0.6 # homeassistant.components.sensor.yr xmltodict + +# homeassistant.components.zeroconf +zeroconf==0.17.5