diff --git a/.coveragerc b/.coveragerc index a18ec476010..d8041b9fe6c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,9 @@ omit = homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present + homeassistant/components/abode.py + homeassistant/components/*/abode.py + homeassistant/components/alarmdecoder.py homeassistant/components/*/alarmdecoder.py @@ -176,6 +179,9 @@ omit = homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py + homeassistant/components/usps.py + homeassistant/components/*/usps.py + homeassistant/components/velbus.py homeassistant/components/*/velbus.py @@ -326,6 +332,7 @@ omit = homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py homeassistant/components/lock/lockitron.py homeassistant/components/lock/sesame.py @@ -383,6 +390,7 @@ omit = homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py + homeassistant/components/notify/hipchat.py homeassistant/components/notify/instapush.py homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py @@ -391,6 +399,7 @@ omit = homeassistant/components/notify/message_bird.py homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nma.py + homeassistant/components/notify/prowl.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushover.py @@ -517,9 +526,9 @@ omit = homeassistant/components/sensor/uber.py homeassistant/components/sensor/upnp.py homeassistant/components/sensor/ups.py - homeassistant/components/sensor/usps.py homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/waqi.py + homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/sensor/zamg.py diff --git a/.gitignore b/.gitignore index 26efcc25b85..87bc6990ce4 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ docs/build # Windows Explorer desktop.ini +/home-assistant.pyproj +/home-assistant.sln +/.vs/home-assistant/v14 diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py new file mode 100644 index 00000000000..677fcab4f5d --- /dev/null +++ b/homeassistant/components/abode.py @@ -0,0 +1,75 @@ +""" +This component provides basic support for Abode Home Security system. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/abode/ +""" +import logging + +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME + +REQUIREMENTS = ['abodepy==0.7.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by goabode.com" + +DOMAIN = 'abode' +DEFAULT_NAME = 'Abode' +DATA_ABODE = 'data_abode' +DEFAULT_ENTITY_NAMESPACE = 'abode' + +NOTIFICATION_ID = 'abode_notification' +NOTIFICATION_TITLE = 'Abode Security Setup' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up Abode component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + try: + data = AbodeData(username, password) + hass.data[DATA_ABODE] = data + + for component in ['binary_sensor', 'alarm_control_panel']: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + return True + + +class AbodeData: + """Shared Abode data.""" + + def __init__(self, username, password): + """Initialize Abode oject.""" + import abodepy + + self.abode = abodepy.Abode(username, password) + self.devices = self.abode.get_devices() + + _LOGGER.debug("Abode Security set up with %s devices", + len(self.devices)) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 39c86f3215f..005048ba8c1 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) + SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT) from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -31,6 +32,7 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_DISARM: 'alarm_disarm', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', + SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night', SERVICE_ALARM_TRIGGER: 'alarm_trigger' } @@ -81,6 +83,18 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) +@bind_hass +def alarm_arm_night(hass, code=None, entity_id=None): + """Send the alarm the command for arm night.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) + + @bind_hass def alarm_trigger(hass, code=None, entity_id=None): """Send the alarm the command for trigger.""" @@ -187,6 +201,17 @@ class AlarmControlPanel(Entity): """ return self.hass.async_add_job(self.alarm_arm_away, code) + def alarm_arm_night(self, code=None): + """Send arm night command.""" + raise NotImplementedError() + + def async_alarm_arm_night(self, code=None): + """Send arm night command. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.alarm_arm_night, code) + def alarm_trigger(self, code=None): """Send alarm trigger command.""" raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/abode.py b/homeassistant/components/alarm_control_panel/abode.py new file mode 100644 index 00000000000..7d7ce931c20 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/abode.py @@ -0,0 +1,82 @@ +""" +This component provides HA alarm_control_panel support for Abode System. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.abode/ +""" +import logging + +from homeassistant.components.abode import (DATA_ABODE, DEFAULT_NAME) +from homeassistant.const import (STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) +import homeassistant.components.alarm_control_panel as alarm + +DEPENDENCIES = ['abode'] + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:security' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for an Abode device.""" + data = hass.data.get(DATA_ABODE) + + add_devices([AbodeAlarm(hass, data, data.abode.get_alarm())]) + + +class AbodeAlarm(alarm.AlarmControlPanel): + """An alarm_control_panel implementation for Abode.""" + + def __init__(self, hass, data, device): + """Initialize the alarm control panel.""" + super(AbodeAlarm, self).__init__() + self._device = device + self._name = "{0}".format(DEFAULT_NAME) + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + if self._device.mode == "standby": + state = STATE_ALARM_DISARMED + elif self._device.mode == "away": + state = STATE_ALARM_ARMED_AWAY + elif self._device.mode == "home": + state = STATE_ALARM_ARMED_HOME + else: + state = None + return state + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._device.set_standby() + self.schedule_update_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._device.set_home() + self.schedule_update_ha_state() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._device.set_away() + self.schedule_update_ha_state() + + def update(self): + """Update the device state.""" + self._device.refresh() diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 8ea472a7b19..fe7db95651b 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) -REQUIREMENTS = ['pythonegardia==1.0.17'] +REQUIREMENTS = ['pythonegardia==1.0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index c87aea862d5..97820ab4b2b 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -12,9 +12,10 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm import homeassistant.util.dt as dt_util from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, - CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, + CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, + CONF_DISARM_AFTER_TRIGGER) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time @@ -87,7 +88,8 @@ class ManualAlarm(alarm.AlarmControlPanel): def state(self): """Return the state of the device.""" if self._state in (STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY) and \ + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT) and \ self._pending_time and self._state_ts + self._pending_time > \ dt_util.utcnow(): return STATE_ALARM_PENDING @@ -145,6 +147,20 @@ class ManualAlarm(alarm.AlarmControlPanel): self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT): + return + + self._state = STATE_ALARM_ARMED_NIGHT + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time) + def alarm_trigger(self, code=None): """Send alarm trigger command. No code needed.""" self._pre_trigger_state = self._state diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 6cc3946ca66..19c3ca0233d 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -31,6 +31,17 @@ alarm_arm_away: description: An optional code to arm away the alarm control panel with example: 1234 +alarm_arm_night: + description: Send the alarm the command for arm night + + fields: + entity_id: + description: Name of alarm control panel to arm night + example: 'alarm_control_panel.downstairs' + code: + description: An optional code to arm night the alarm control panel with + example: 1234 + alarm_trigger: description: Send the alarm the command for trigger diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 5eb2e9fe7d3..7f4e4dfa756 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -16,7 +16,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['simplisafe-python==1.0.4'] +REQUIREMENTS = ['simplisafe-python==1.0.5'] _LOGGER = logging.getLogger(__name__) @@ -89,11 +89,11 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): def state(self): """Return the state of the device.""" status = self.simplisafe.state() - if status == 'Off': + if status == 'off': state = STATE_ALARM_DISARMED - elif status == 'Home': + elif status == 'home': state = STATE_ALARM_ARMED_HOME - elif status == 'Away': + elif status == 'away': state = STATE_ALARM_ARMED_AWAY else: state = STATE_UNKNOWN diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 9c0b5108fee..05dc8aeef20 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -13,8 +13,8 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, - CONF_NAME) + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME) REQUIREMENTS = ['total_connect_client==0.11'] @@ -74,6 +74,12 @@ class TotalConnect(alarm.AlarmControlPanel): state = STATE_ALARM_ARMED_HOME elif status == self._client.ARMED_AWAY: state = STATE_ALARM_ARMED_AWAY + elif status == self._client.ARMED_STAY_NIGHT: + state = STATE_ALARM_ARMED_NIGHT + elif status == self._client.ARMING: + state = STATE_ALARM_ARMING + elif status == self._client.DISARMING: + state = STATE_ALARM_DISARMING else: state = STATE_UNKNOWN @@ -90,3 +96,7 @@ class TotalConnect(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" self._client.arm_away() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._client.arm_stay_night() diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index c5f40ca5db8..7a2ff7610f7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -91,7 +91,7 @@ def request_configuration(hass, config, atv, credentials): hass.async_add_job(configurator.request_done, instance) instance = configurator.request_config( - hass, 'Apple TV Authentication', configuration_callback, + 'Apple TV Authentication', configuration_callback, description='Please enter PIN code shown on screen.', submit_caption='Confirm', fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index d83e07989e6..eaf85937658 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -110,7 +110,7 @@ def request_configuration(hass, name, host, serialnumber): title = '{} ({})'.format(name, host) request_id = configurator.request_config( - hass, title, configuration_callback, + title, configuration_callback, description='Functionality: ' + str(AXIS_INCLUDE), entity_picture="/static/images/logo_axis.png", link_name='Axis platform documentation', diff --git a/homeassistant/components/binary_sensor/abode.py b/homeassistant/components/binary_sensor/abode.py new file mode 100644 index 00000000000..9abff53026d --- /dev/null +++ b/homeassistant/components/binary_sensor/abode.py @@ -0,0 +1,81 @@ +""" +This component provides HA binary_sensor support for Abode Security System. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.abode/ +""" +import logging + +from homeassistant.components.abode import (CONF_ATTRIBUTION, DATA_ABODE) +from homeassistant.const import (ATTR_ATTRIBUTION) +from homeassistant.components.binary_sensor import (BinarySensorDevice) + +DEPENDENCIES = ['abode'] + +_LOGGER = logging.getLogger(__name__) + +# Sensor types: Name, device_class +SENSOR_TYPES = { + 'Door Contact': 'opening', + 'Motion Camera': 'motion', +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for an Abode device.""" + data = hass.data.get(DATA_ABODE) + + sensors = [] + for sensor in data.devices: + _LOGGER.debug('Sensor type %s', sensor.type) + if sensor.type in ['Door Contact', 'Motion Camera']: + sensors.append(AbodeBinarySensor(hass, data, sensor)) + + _LOGGER.debug('Adding %d sensors', len(sensors)) + add_devices(sensors) + + +class AbodeBinarySensor(BinarySensorDevice): + """A binary sensor implementation for Abode device.""" + + def __init__(self, hass, data, device): + """Initialize a sensor for Abode device.""" + super(AbodeBinarySensor, self).__init__() + self._device = device + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return "{0} {1}".format(self._device.type, self._device.name) + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + if self._device.type == 'Door Contact': + return self._device.status != 'Closed' + elif self._device.type == 'Motion Camera': + return self._device.get_value('motion_event') == '1' + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return SENSOR_TYPES.get(self._device.type) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {} + attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs['device_id'] = self._device.device_id + attrs['battery_low'] = self._device.battery_low + + return attrs + + def update(self): + """Update the device state.""" + self._device.refresh() diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 767ed858ec7..4b83f0c8f2d 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -4,62 +4,27 @@ Support for MySensors binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mysensors/ """ -import logging - from homeassistant.components import mysensors -from homeassistant.components.binary_sensor import (DEVICE_CLASSES, +from homeassistant.components.binary_sensor import (DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for sensors.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DOOR: [set_req.V_TRIPPED], - pres.S_MOTION: [set_req.V_TRIPPED], - pres.S_SMOKE: [set_req.V_TRIPPED], - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_SPRINKLER: [set_req.V_TRIPPED], - pres.S_WATER_LEAK: [set_req.V_TRIPPED], - pres.S_SOUND: [set_req.V_TRIPPED], - pres.S_VIBRATION: [set_req.V_TRIPPED], - pres.S_MOISTURE: [set_req.V_TRIPPED], - }) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsBinarySensor, add_devices)) + """Setup the mysensors platform for binary sensors.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsBinarySensor, + add_devices=add_devices) class MySensorsBinarySensor( - mysensors.MySensorsDeviceEntity, BinarySensorDevice): + mysensors.MySensorsEntity, BinarySensorDevice): """Represent the value of a MySensors Binary Sensor child node.""" @property def is_on(self): """Return True if the binary sensor is on.""" - if self.value_type in self._values: - return self._values[self.value_type] == STATE_ON - return False + return self._values.get(self.value_type) == STATE_ON @property def device_class(self): diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 81cc8fd8798..f48525d41a8 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -6,13 +6,12 @@ https://home-assistant.io/components/binary_sensor.workday/ """ import asyncio import logging -import datetime +from datetime import datetime, timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, WEEKDAYS -import homeassistant.util.dt as dt_util from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv @@ -39,11 +38,14 @@ CONF_EXCLUDES = 'excludes' DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday'] DEFAULT_NAME = 'Workday Sensor' ALLOWED_DAYS = WEEKDAYS + ['holiday'] +CONF_OFFSET = 'days_offset' +DEFAULT_OFFSET = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), vol.Optional(CONF_PROVINCE, default=None): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): @@ -60,8 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): province = config.get(CONF_PROVINCE) workdays = config.get(CONF_WORKDAYS) excludes = config.get(CONF_EXCLUDES) + days_offset = config.get(CONF_OFFSET) - year = datetime.datetime.now().year + year = (datetime.now() + timedelta(days=days_offset)).year obj_holidays = getattr(holidays, country)(years=year) if province: @@ -85,7 +88,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.debug("%s %s", date, name) add_devices([IsWorkdaySensor( - obj_holidays, workdays, excludes, sensor_name)], True) + obj_holidays, workdays, excludes, days_offset, sensor_name)], True) def day_to_string(day): @@ -99,12 +102,13 @@ def day_to_string(day): class IsWorkdaySensor(BinarySensorDevice): """Implementation of a Workday sensor.""" - def __init__(self, obj_holidays, workdays, excludes, name): + def __init__(self, obj_holidays, workdays, excludes, days_offset, name): """Initialize the Workday sensor.""" self._name = name self._obj_holidays = obj_holidays self._workdays = workdays self._excludes = excludes + self._days_offset = days_offset self._state = None @property @@ -135,6 +139,16 @@ class IsWorkdaySensor(BinarySensorDevice): return False + @property + def state_attributes(self): + """Return the attributes of the entity.""" + # return self._attributes + return { + CONF_WORKDAYS: self._workdays, + CONF_EXCLUDES: self._excludes, + CONF_OFFSET: self._days_offset + } + @asyncio.coroutine def async_update(self): """Get date and look whether it is a holiday.""" @@ -142,11 +156,12 @@ class IsWorkdaySensor(BinarySensorDevice): self._state = False # Get iso day of the week (1 = Monday, 7 = Sunday) - day = datetime.datetime.today().isoweekday() - 1 + date = datetime.today() + timedelta(days=self._days_offset) + day = date.isoweekday() - 1 day_of_week = day_to_string(day) - if self.is_include(day_of_week, dt_util.now()): + if self.is_include(day_of_week, date): self._state = True - if self.is_exclude(day_of_week, dt_util.now()): + if self.is_exclude(day_of_week, date): self._state = False diff --git a/homeassistant/components/camera/usps.py b/homeassistant/components/camera/usps.py new file mode 100644 index 00000000000..545ea9798de --- /dev/null +++ b/homeassistant/components/camera/usps.py @@ -0,0 +1,94 @@ +""" +Support for a camera made up of usps mail images. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/camera.usps/ +""" +from datetime import timedelta +import logging + +from homeassistant.components.camera import Camera +from homeassistant.components.usps import DATA_USPS + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['usps'] + +SCAN_INTERVAL = timedelta(seconds=10) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up USPS mail camera.""" + if discovery_info is None: + return + + usps = hass.data[DATA_USPS] + add_devices([USPSCamera(usps)]) + + +class USPSCamera(Camera): + """Representation of the images available from USPS.""" + + def __init__(self, usps): + """Initialize the USPS camera images.""" + super().__init__() + + self._usps = usps + self._name = self._usps.name + self._session = self._usps.session + + self._mail_img = [] + self._last_mail = None + self._mail_index = 0 + self._mail_count = 0 + + self._timer = None + + def camera_image(self): + """Update the camera's image if it has changed.""" + self._usps.update() + try: + self._mail_count = len(self._usps.mail) + except TypeError: + # No mail + return None + + if self._usps.mail != self._last_mail: + # Mail items must have changed + self._mail_img = [] + if len(self._usps.mail) >= 1: + self._last_mail = self._usps.mail + for article in self._usps.mail: + _LOGGER.debug("Fetching article image: %s", article) + img = self._session.get(article['image']).content + self._mail_img.append(img) + + try: + return self._mail_img[self._mail_index] + except IndexError: + return None + + @property + def name(self): + """Return the name of this camera.""" + return '{} mail'.format(self._name) + + @property + def model(self): + """Return date of mail as model.""" + try: + return 'Date: {}'.format(self._usps.mail[0]['date']) + except IndexError: + return None + + @property + def should_poll(self): + """Update the mail image index periodically.""" + return True + + def update(self): + """Update mail image index.""" + if self._mail_index < (self._mail_count - 1): + self._mail_index += 1 + else: + self._mail_index = 0 diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 82ed8a94e2b..d4316c2cfba 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -4,15 +4,11 @@ MySensors platform that offers a Climate (MySensors-HVAC) component. For more details about this platform, please refer to the documentation https://home-assistant.io/components/climate.mysensors/ """ -import logging - from homeassistant.components import mysensors from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_OFF, STATE_AUTO, ClimateDevice, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE - -_LOGGER = logging.getLogger(__name__) + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, + STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT DICT_HA_TO_MYS = { STATE_AUTO: 'AutoChangeOver', @@ -29,28 +25,12 @@ DICT_MYS_TO_HA = { def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the mysensors climate.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - if float(gateway.protocol_version) < 1.5: - continue - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_HVAC: [set_req.V_HVAC_FLOW_STATE], - } - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsHVAC, add_devices)) + """Setup the mysensors climate.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) -class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): +class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): """Representation of a MySensors HVAC.""" @property @@ -84,26 +64,28 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) if temp is None: temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) - return float(temp) + return float(temp) if temp is not None else None @property def target_temperature_high(self): """Return the highbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_HEAT in self._values: - return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL)) + temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + return float(temp) if temp is not None else None @property def target_temperature_low(self): """Return the lowbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_COOL in self._values: - return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT)) + temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + return float(temp) if temp is not None else None @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._values.get(self.gateway.const.SetReq.V_HVAC_FLOW_STATE) + return self._values.get(self.value_type) @property def operation_list(self): @@ -128,7 +110,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): high = kwargs.get(ATTR_TARGET_TEMP_HIGH) heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL) - updates = () + updates = [] if temp is not None: if heat is not None: # Set HEAT Target temperature @@ -146,7 +128,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # optimistically assume that device has changed state self._values[value_type] = value self.schedule_update_ha_state() @@ -156,54 +138,22 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" - set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_FLOW_STATE, + self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[operation_mode]) if self.gateway.optimistic: - # optimistically assume that switch has changed state - self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode + # optimistically assume that device has changed state + self._values[self.value_type] = operation_mode self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - set_req = self.gateway.const.SetReq - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == set_req.V_HVAC_FLOW_STATE: - self._values[value_type] = DICT_MYS_TO_HA[value] - else: - self._values[value_type] = value - - def set_humidity(self, humidity): - """Set new target humidity.""" - _LOGGER.error("Service Not Implemented yet") - - def set_swing_mode(self, swing_mode): - """Set new target swing operation.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_away_mode_on(self): - """Turn away mode on.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_away_mode_off(self): - """Turn away mode off.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_aux_heat_on(self): - """Turn auxillary heater on.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_aux_heat_off(self): - """Turn auxillary heater off.""" - _LOGGER.error("Service Not Implemented yet") + super().update() + self._values[self.value_type] = DICT_MYS_TO_HA[ + self._values[self.value_type]] diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 0bc44501e28..9e447c8936a 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] -SECTIONS = ('core', 'group', 'hassbian', 'automation') +SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script') ON_DEMAND = ('zwave') diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py new file mode 100644 index 00000000000..345c8e4a849 --- /dev/null +++ b/homeassistant/components/config/script.py @@ -0,0 +1,19 @@ +"""Provide configuration end points for scripts.""" +import asyncio + +from homeassistant.components.config import EditKeyBasedConfigView +from homeassistant.components.script import SCRIPT_ENTRY_SCHEMA, async_reload +import homeassistant.helpers.config_validation as cv + + +CONFIG_PATH = 'scripts.yaml' + + +@asyncio.coroutine +def async_setup(hass): + """Set up the script config API.""" + hass.http.register_view(EditKeyBasedConfigView( + 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, + post_write_hook=async_reload + )) + return True diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 660a62a5b89..2da8967bddf 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -7,19 +7,21 @@ A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ import asyncio +import functools as ft import logging from homeassistant.core import callback as async_callback from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ ATTR_ENTITY_PICTURE from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util.async import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) -_REQUESTS = {} _KEY_INSTANCE = 'configurator' +DATA_REQUESTS = 'configurator_requests' + ATTR_CONFIGURE_ID = 'configure_id' ATTR_DESCRIPTION = 'description' ATTR_DESCRIPTION_IMAGE = 'description_image' @@ -39,63 +41,89 @@ STATE_CONFIGURED = 'configured' @bind_hass -def request_config( - hass, name, callback, description=None, description_image=None, +@async_callback +def async_request_config( + hass, name, callback=None, description=None, description_image=None, submit_caption=None, fields=None, link_name=None, link_url=None, entity_picture=None): """Create a new request for configuration. Will return an ID to be used for sequent calls. """ - instance = run_callback_threadsafe(hass.loop, - _async_get_instance, - hass).result() + instance = hass.data.get(_KEY_INSTANCE) - request_id = instance.request_config( + if instance is None: + instance = hass.data[_KEY_INSTANCE] = Configurator(hass) + + request_id = instance.async_request_config( name, callback, description, description_image, submit_caption, fields, link_name, link_url, entity_picture) - _REQUESTS[request_id] = instance + if DATA_REQUESTS not in hass.data: + hass.data[DATA_REQUESTS] = {} + + hass.data[DATA_REQUESTS][request_id] = instance return request_id -def notify_errors(request_id, error): +@bind_hass +def request_config(hass, *args, **kwargs): + """Create a new request for configuration. + + Will return an ID to be used for sequent calls. + """ + return run_callback_threadsafe( + hass.loop, ft.partial(async_request_config, hass, *args, **kwargs) + ).result() + + +@bind_hass +@async_callback +def async_notify_errors(hass, request_id, error): """Add errors to a config request.""" try: - _REQUESTS[request_id].notify_errors(request_id, error) + hass.data[DATA_REQUESTS][request_id].async_notify_errors( + request_id, error) except KeyError: # If request_id does not exist pass -def request_done(request_id): +@bind_hass +def notify_errors(hass, request_id, error): + """Add errors to a config request.""" + return run_callback_threadsafe( + hass.loop, async_notify_errors, hass, request_id, error + ).result() + + +@bind_hass +@async_callback +def async_request_done(hass, request_id): """Mark a configuration request as done.""" try: - _REQUESTS.pop(request_id).request_done(request_id) + hass.data[DATA_REQUESTS].pop(request_id).async_request_done(request_id) except KeyError: # If request_id does not exist pass +@bind_hass +def request_done(hass, request_id): + """Mark a configuration request as done.""" + return run_callback_threadsafe( + hass.loop, async_request_done, hass, request_id + ).result() + + @asyncio.coroutine def async_setup(hass, config): """Set up the configurator component.""" return True -@async_callback -def _async_get_instance(hass): - """Get an instance per hass object.""" - instance = hass.data.get(_KEY_INSTANCE) - - if instance is None: - instance = hass.data[_KEY_INSTANCE] = Configurator(hass) - - return instance - - class Configurator(object): """The class to keep track of current configuration requests.""" @@ -105,14 +133,16 @@ class Configurator(object): self._cur_id = 0 self._requests = {} hass.services.async_register( - DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) + DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_call) - def request_config( + @async_callback + def async_request_config( self, name, callback, description, description_image, submit_caption, fields, link_name, link_url, entity_picture): """Set up a request for configuration.""" - entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) + entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, name, hass=self.hass) if fields is None: fields = [] @@ -138,11 +168,12 @@ class Configurator(object): ] if value is not None }) - self.hass.states.set(entity_id, STATE_CONFIGURE, data) + self.hass.states.async_set(entity_id, STATE_CONFIGURE, data) return request_id - def notify_errors(self, request_id, error): + @async_callback + def async_notify_errors(self, request_id, error): """Update the state with errors.""" if not self._validate_request_id(request_id): return @@ -154,9 +185,10 @@ class Configurator(object): new_data = dict(state.attributes) new_data[ATTR_ERRORS] = error - self.hass.states.set(entity_id, STATE_CONFIGURE, new_data) + self.hass.states.async_set(entity_id, STATE_CONFIGURE, new_data) - def request_done(self, request_id): + @async_callback + def async_request_done(self, request_id): """Remove the configuration request.""" if not self._validate_request_id(request_id): return @@ -167,15 +199,16 @@ class Configurator(object): # the result fo the service call (current design limitation). # Instead, we will set it to configured to give as feedback but delete # it shortly after so that it is deleted when the client updates. - self.hass.states.set(entity_id, STATE_CONFIGURED) + self.hass.states.async_set(entity_id, STATE_CONFIGURED) def deferred_remove(event): """Remove the request state.""" - self.hass.states.remove(entity_id) + self.hass.states.async_remove(entity_id) - self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove) + self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) - def handle_service_call(self, call): + @async_callback + def async_handle_service_call(self, call): """Handle a configure service call.""" request_id = call.data.get(ATTR_CONFIGURE_ID) @@ -186,8 +219,8 @@ class Configurator(object): entity_id, fields, callback = self._requests[request_id] # field validation goes here? - - self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) + if callback: + self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index f48a2110eca..cd4ff62b3e9 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -4,42 +4,18 @@ Support for MySensors covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mysensors/ """ -import logging - from homeassistant.components import mysensors -from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.cover import CoverDevice, ATTR_POSITION, DOMAIN from homeassistant.const import STATE_ON, STATE_OFF -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = [] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for covers.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT], - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS], - }) - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsCover, add_devices)) + """Setup the mysensors platform for covers.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices) -class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): +class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 769c2fc4ed6..f9e059d3927 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, MATCH_ALL, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, - STATE_OPEN, STATE_CLOSED) + CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id @@ -39,6 +39,8 @@ CLOSE_ACTION = 'close_cover' STOP_ACTION = 'stop_cover' POSITION_ACTION = 'set_cover_position' TILT_ACTION = 'set_cover_tilt_position' +CONF_TILT_OPTIMISTIC = 'tilt_optimistic' + CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position' CONF_OPEN_OR_CLOSE = 'open_or_close' @@ -56,6 +58,8 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, @@ -83,11 +87,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): stop_action = device_config.get(STOP_ACTION) position_action = device_config.get(POSITION_ACTION) tilt_action = device_config.get(TILT_ACTION) - - if position_template is None and state_template is None: - _LOGGER.error('Must specify either %s' or '%s', - CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE) - continue + optimistic = device_config.get(CONF_OPTIMISTIC) + tilt_optimistic = device_config.get(CONF_TILT_OPTIMISTIC) if position_action is None and open_action is None: _LOGGER.error('Must specify at least one of %s' or '%s', @@ -125,7 +126,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device, friendly_name, state_template, position_template, tilt_template, icon_template, open_action, close_action, stop_action, - position_action, tilt_action, entity_ids + position_action, tilt_action, + optimistic, tilt_optimistic, entity_ids ) ) if not covers: @@ -142,7 +144,8 @@ class CoverTemplate(CoverDevice): def __init__(self, hass, device_id, friendly_name, state_template, position_template, tilt_template, icon_template, open_action, close_action, stop_action, - position_action, tilt_action, entity_ids): + position_action, tilt_action, + optimistic, tilt_optimistic, entity_ids): """Initialize the Template cover.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -167,6 +170,9 @@ class CoverTemplate(CoverDevice): self._tilt_script = None if tilt_action is not None: self._tilt_script = Script(hass, tilt_action) + self._optimistic = (optimistic or + (not state_template and not position_template)) + self._tilt_optimistic = tilt_optimistic or not tilt_template self._icon = None self._position = None self._tilt_value = None @@ -260,19 +266,23 @@ class CoverTemplate(CoverDevice): def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - self.hass.async_add_job(self._open_script.async_run()) + yield from self._open_script.async_run() elif self._position_script: - self.hass.async_add_job(self._position_script.async_run( - {"position": 100})) + yield from self._position_script.async_run({"position": 100}) + if self._optimistic: + self._position = 100 + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - self.hass.async_add_job(self._close_script.async_run()) + yield from self._close_script.async_run() elif self._position_script: - self.hass.async_add_job(self._position_script.async_run( - {"position": 0})) + yield from self._position_script.async_run({"position": 0}) + if self._optimistic: + self._position = 0 + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_stop_cover(self, **kwargs): @@ -284,29 +294,35 @@ class CoverTemplate(CoverDevice): def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] - self.hass.async_add_job(self._position_script.async_run( - {"position": self._position})) + yield from self._position_script.async_run( + {"position": self._position}) + if self._optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" self._tilt_value = 0 - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run( + {"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_update(self): diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 891f1b22775..071edf42642 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -6,28 +6,32 @@ https://home-assistant.io/components/device_tracker.automatic/ """ import asyncio from datetime import timedelta +import json import logging +import os +from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_MAC, ATTR_GPS, ATTR_GPS_ACCURACY) -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['aioautomatic==0.4.0'] +REQUIREMENTS = ['aioautomatic==0.6.0'] +DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) CONF_CLIENT_ID = 'client_id' CONF_SECRET = 'secret' CONF_DEVICES = 'devices' +CONF_CURRENT_LOCATION = 'current_location' DEFAULT_TIMEOUT = 5 @@ -38,38 +42,74 @@ ATTR_FUEL_LEVEL = 'fuel_level' EVENT_AUTOMATIC_UPDATE = 'automatic_update' +AUTOMATIC_CONFIG_FILE = '.automatic/session-{}.json' + +DATA_CONFIGURING = 'automatic_configurator_clients' +DATA_REFRESH_TOKEN = 'refresh_token' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_SECRET): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, vol.Optional(CONF_DEVICES, default=None): vol.All( cv.ensure_list, [cv.string]) }) +def _get_refresh_token_from_file(hass, filename): + """Attempt to load session data from file.""" + path = hass.config.path(filename) + + if not os.path.isfile(path): + return None + + try: + with open(path) as data_file: + data = json.load(data_file) + if data is None: + return None + + return data.get(DATA_REFRESH_TOKEN) + except ValueError: + return None + + +def _write_refresh_token_to_file(hass, filename, refresh_token): + """Attempt to store session data to file.""" + path = hass.config.path(filename) + + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w+') as data_file: + json.dump({ + DATA_REFRESH_TOKEN: refresh_token + }, data_file) + + @asyncio.coroutine def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" import aioautomatic + hass.http.register_view(AutomaticAuthCallbackView()) + + scope = FULL_SCOPE if config.get(CONF_CURRENT_LOCATION) else DEFAULT_SCOPE + client = aioautomatic.Client( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_SECRET], client_session=async_get_clientsession(hass), request_kwargs={'timeout': DEFAULT_TIMEOUT}) - try: - try: - session = yield from client.create_session_from_password( - FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD]) - except aioautomatic.exceptions.ForbiddenError as exc: - if not str(exc).startswith("invalid_scope"): - raise exc - _LOGGER.info("Client not authorized for current_location scope. " - "location:updated events will not be received.") - session = yield from client.create_session_from_password( - DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD]) + filename = AUTOMATIC_CONFIG_FILE.format(config[CONF_CLIENT_ID]) + refresh_token = yield from hass.async_add_job( + _get_refresh_token_from_file, hass, filename) + + @asyncio.coroutine + def initialize_data(session): + """Initialize the AutomaticData object from the created session.""" + hass.async_add_job( + _write_refresh_token_to_file, hass, filename, + session.refresh_token) data = AutomaticData( hass, client, session, config[CONF_DEVICES], async_see) @@ -77,26 +117,86 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): vehicles = yield from session.get_vehicles() for vehicle in vehicles: hass.async_add_job(data.load_vehicle(vehicle)) - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - return False - @callback - def ws_connect(event): - """Open the websocket connection.""" - hass.async_add_job(data.ws_connect()) + # Create a task instead of adding a tracking job, since this task will + # run until the websocket connection is closed. + hass.loop.create_task(data.ws_connect()) - @callback - def ws_close(event): - """Close the websocket connection.""" - hass.async_add_job(data.ws_close()) + if refresh_token is not None: + try: + session = yield from client.create_session_from_refresh_token( + refresh_token) + yield from initialize_data(session) + return True + except aioautomatic.exceptions.AutomaticError as err: + _LOGGER.error(str(err)) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, ws_connect) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, ws_close) + configurator = hass.components.configurator + request_id = configurator.async_request_config( + "Automatic", description=( + "Authorization required for Automatic device tracker."), + link_name="Click here to authorize Home Assistant.", + link_url=client.generate_oauth_url(scope), + entity_picture="/static/images/logo_automatic.png", + ) + @asyncio.coroutine + def initialize_callback(code, state): + """Callback after OAuth2 response is returned.""" + try: + session = yield from client.create_session_from_oauth_code( + code, state) + yield from initialize_data(session) + configurator.async_request_done(request_id) + except aioautomatic.exceptions.AutomaticError as err: + _LOGGER.error(str(err)) + configurator.async_notify_errors(request_id, str(err)) + return False + + if DATA_CONFIGURING not in hass.data: + hass.data[DATA_CONFIGURING] = {} + + hass.data[DATA_CONFIGURING][client.state] = initialize_callback return True +class AutomaticAuthCallbackView(HomeAssistantView): + """Handle OAuth finish callback requests.""" + + requires_auth = False + url = '/api/automatic/callback' + name = 'api:automatic:callback' + + @callback + def get(self, request): # pylint: disable=no-self-use + """Finish OAuth callback request.""" + hass = request.app['hass'] + params = request.query + response = web.HTTPFound('/states') + + if 'state' not in params or 'code' not in params: + if 'error' in params: + _LOGGER.error( + "Error authorizing Automatic: %s", params['error']) + return response + else: + _LOGGER.error( + "Error authorizing Automatic. Invalid response returned.") + return response + + if DATA_CONFIGURING not in hass.data or \ + params['state'] not in hass.data[DATA_CONFIGURING]: + _LOGGER.error("Automatic configuration request not found.") + return response + + code = params['code'] + state = params['state'] + initialize_callback = hass.data[DATA_CONFIGURING][state] + hass.async_add_job(initialize_callback(code, state)) + + return response + + class AutomaticData(object): """A class representing an Automatic cloud service connection.""" @@ -115,6 +215,8 @@ class AutomaticData(object): lambda name, event: self.hass.async_add_job( self.handle_event(name, event))) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) + @asyncio.coroutine def handle_event(self, name, event): """Coroutine to update state for a realtime event.""" diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 194a2f4bfac..f20dad1fceb 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -19,7 +19,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify import homeassistant.util.dt as dt_util from homeassistant.util.location import distance -from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -209,7 +208,7 @@ class Icloud(DeviceScanner): if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) - configurator = get_component('configurator') + configurator = self.hass.components.configurator configurator.request_done(request_id) # Trigger the next step immediately @@ -217,7 +216,7 @@ class Icloud(DeviceScanner): def icloud_need_trusted_device(self): """We need a trusted device.""" - configurator = get_component('configurator') + configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return @@ -229,7 +228,7 @@ class Icloud(DeviceScanner): devicesstring += "{}: {};".format(i, devicename) _CONFIGURING[self.accountname] = configurator.request_config( - self.hass, 'iCloud {}'.format(self.accountname), + 'iCloud {}'.format(self.accountname), self.icloud_trusted_device_callback, description=( 'Please choose your trusted device by entering' @@ -259,17 +258,17 @@ class Icloud(DeviceScanner): if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) - configurator = get_component('configurator') + configurator = self.hass.components.configurator configurator.request_done(request_id) def icloud_need_verification_code(self): """Return the verification code.""" - configurator = get_component('configurator') + configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return _CONFIGURING[self.accountname] = configurator.request_config( - self.hass, 'iCloud {}'.format(self.accountname), + 'iCloud {}'.format(self.accountname), self.icloud_verification_callback, description=('Please enter the validation code:'), entity_picture="/static/images/config_icloud.png", diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index 4503c4d1b26..f68eb361ca0 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -4,61 +4,51 @@ Support for tracking MySensors devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mysensors/ """ -import logging - from homeassistant.components import mysensors +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.util import slugify -DEPENDENCIES = ['mysensors'] - -_LOGGER = logging.getLogger(__name__) - def setup_scanner(hass, config, see, discovery_info=None): - """Set up the MySensors tracker.""" - def mysensors_callback(gateway, msg): - """Set up callback for mysensors platform.""" - node = gateway.sensors[msg.node_id] - if node.sketch_name is None: - _LOGGER.debug("No sketch_name: node %s", msg.node_id) - return + """Set up the MySensors device scanner.""" + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsDeviceScanner, + device_args=(see, )) + if not new_devices: + return False - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - - child = node.children.get(msg.child_id) - if child is None: - return - position = child.values.get(set_req.V_POSITION) - if child.type != pres.S_GPS or position is None: - return - try: - latitude, longitude, _ = position.split(',') - except ValueError: - _LOGGER.error("Payload for V_POSITION %s is not of format " - "latitude, longitude, altitude", position) - return - name = '{} {} {}'.format( - node.sketch_name, msg.node_id, child.id) - attr = { - mysensors.ATTR_CHILD_ID: child.id, - mysensors.ATTR_DESCRIPTION: child.description, - mysensors.ATTR_DEVICE: gateway.device, - mysensors.ATTR_NODE_ID: msg.node_id, - } - see( - dev_id=slugify(name), - host_name=name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=attr - ) - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - - for gateway in gateways: - if float(gateway.protocol_version) < 2.0: - continue - gateway.platform_callbacks.append(mysensors_callback) + for device in new_devices: + dev_id = ( + id(device.gateway), device.node_id, device.child_id, + device.value_type) + dispatcher_connect( + hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), + device.update_callback) return True + + +class MySensorsDeviceScanner(mysensors.MySensorsDevice): + """Represent a MySensors scanner.""" + + def __init__(self, see, *args): + """Set up instance.""" + super().__init__(*args) + self.see = see + + def update_callback(self): + """Update the device.""" + self.update() + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + position = child.values[self.value_type] + latitude, longitude, _ = position.split(',') + + self.see( + dev_id=slugify(self.name), + host_name=self.name, + gps=(latitude, longitude), + battery=node.battery_level, + attributes=self.device_state_attributes + ) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index f0c95f7de3d..9e74299e6bc 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -13,10 +13,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.const import CONF_API_KEY -from homeassistant.loader import get_component from homeassistant.util import Throttle -REQUIREMENTS = ['python-ecobee-api==0.0.7'] +REQUIREMENTS = ['python-ecobee-api==0.0.8'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -41,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema({ def request_configuration(network, hass, config): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator if 'ecobee' in _CONFIGURING: configurator.notify_errors( _CONFIGURING['ecobee'], "Failed to register, please try again.") @@ -56,7 +55,7 @@ def request_configuration(network, hass, config): setup_ecobee(hass, network, config) _CONFIGURING['ecobee'] = configurator.request_config( - hass, "Ecobee", ecobee_configuration_callback, + "Ecobee", ecobee_configuration_callback, description=( 'Please authorize this app at https://www.ecobee.com/consumer' 'portal/index.html with pin code: ' + network.pin), @@ -73,7 +72,7 @@ def setup_ecobee(hass, network, config): return if 'ecobee' in _CONFIGURING: - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(_CONFIGURING.pop('ecobee')) hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 31d8ab60e30..f8d41424064 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -2,7 +2,6 @@ import threading import socket import logging -import os import select from aiohttp import web @@ -86,18 +85,6 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 advertise_ip, advertise_port).replace("\n", "\r\n") \ .encode('utf-8') - # Set up a pipe for signaling to the receiver that it's time to - # shutdown. Essentially, we place the SSDP socket into nonblocking - # mode and use select() to wait for data to arrive on either the SSDP - # socket or the pipe. If data arrives on either one, select() returns - # and tells us which filenos have data ready to read. - # - # When we want to stop the responder, we write data to the pipe, which - # causes the select() to return and indicate that said pipe has data - # ready to be read, which indicates to us that the responder needs to - # be shutdown. - self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe() - def run(self): """Run the server.""" # Listen for UDP port 1900 packets sent to SSDP multicast address @@ -119,7 +106,7 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 socket.inet_aton(self.host_ip_addr)) if self.upnp_bind_multicast: - ssdp_socket.bind(("239.255.255.250", 1900)) + ssdp_socket.bind(("", 1900)) else: ssdp_socket.bind((self.host_ip_addr, 1900)) @@ -130,16 +117,13 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 try: read, _, _ = select.select( - [self._interrupted_read_pipe, ssdp_socket], [], - [ssdp_socket]) + [ssdp_socket], [], + [ssdp_socket], 2) - if self._interrupted_read_pipe in read: - # Implies self._interrupted is True - clean_socket_close(ssdp_socket) - return - elif ssdp_socket in read: + if ssdp_socket in read: data, addr = ssdp_socket.recvfrom(1024) else: + # most likely the timeout, so check for interupt continue except socket.error as ex: if self._interrupted: @@ -148,6 +132,9 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 _LOGGER.error("UPNP Responder socket exception occured: %s", ex.__str__) + # without the following continue, a second exception occurs + # because the data object has not been initialized + continue if "M-SEARCH" in data.decode('utf-8'): # SSDP M-SEARCH method received, respond to it with our info @@ -161,7 +148,6 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 """Stop the server.""" # Request for server self._interrupted = True - os.write(self._interrupted_write_pipe, bytes([0])) self.join() diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 87b6163282a..5ffd97ef0e3 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.1'] +REQUIREMENTS = ['pyenvisalink==2.2'] _LOGGER = logging.getLogger(__name__) @@ -74,9 +74,9 @@ CONFIG_SCHEMA = vol.Schema({ vol.All(vol.Coerce(int), vol.Range(min=3, max=4)), vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)), - vol.Optional(CONF_ZONEDUMP_INTERVAL, - default=DEFAULT_ZONEDUMP_INTERVAL): - vol.All(vol.Coerce(int), vol.Range(min=15)), + vol.Optional( + CONF_ZONEDUMP_INTERVAL, + default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int), }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index a18c173ecca..5bdfec08427 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -13,7 +13,6 @@ from homeassistant.components.fan import ( ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, FanEntity) from homeassistant.helpers.entity import ToggleEntity -from homeassistant.loader import get_component import homeassistant.util as util _CONFIGURING = {} @@ -57,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def request_configuration(device_id, insteonhub, model, hass, add_devices_callback): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if device_id in _CONFIGURING: @@ -72,7 +71,7 @@ def request_configuration(device_id, insteonhub, model, hass, add_devices_callback) _CONFIGURING[device_id] = configurator.request_config( - hass, 'Insteon ' + model + ' addr: ' + device_id, + 'Insteon ' + model + ' addr: ' + device_id, insteon_fan_config_callback, description=('Enter a name for ' + model + ' Fan addr: ' + device_id), entity_picture='/static/images/config_insteon.png', @@ -85,7 +84,7 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback): """Set up the fan.""" if device_id in _CONFIGURING: request_id = _CONFIGURING.pop(device_id) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) _LOGGER.info("Device configuration done!") diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index 8b9236fdb32..90cd161fa20 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -16,6 +16,11 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +# Define term used for medium speed. This must be set as the fan component uses +# 'medium' which the ISY does not understand +ISY_SPEED_MEDIUM = 'med' + + VALUE_TO_STATE = { 0: SPEED_OFF, 63: SPEED_LOW, @@ -29,7 +34,7 @@ STATE_TO_VALUE = {} for key in VALUE_TO_STATE: STATE_TO_VALUE[VALUE_TO_STATE[key]] = key -STATES = [SPEED_OFF, SPEED_LOW, 'med', SPEED_HIGH] +STATES = [SPEED_OFF, SPEED_LOW, ISY_SPEED_MEDIUM, SPEED_HIGH] # pylint: disable=unused-argument @@ -93,6 +98,11 @@ class ISYFanDevice(isy.ISYDevice, FanEntity): else: self.speed = self.state + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + class ISYFanProgram(ISYFanDevice): """Representation of an ISY994 fan program.""" diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 45bd651ad95..887d07e5855 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['ha-ffmpeg==1.5'] +REQUIREMENTS = ['ha-ffmpeg==1.7'] DOMAIN = 'ffmpeg' diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 07cd39ca581..bd1b511d332 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,10 +3,10 @@ FINGERPRINTS = { "compatibility.js": "1686167ff210e001f063f5c606b2e74b", "core.js": "2a7d01e45187c7d4635da05065b5e54e", - "frontend.html": "5a2a3d6181cc820f5b3e94d1a50def74", + "frontend.html": "6c8192a4393c9e83516dc8177b75c23d", "mdi.html": "e91f61a039ed0a9936e7ee5360da3870", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-config.html": "ec48185c79000d0cfe5bbf38c7974944", + "panels/ha-panel-config.html": "bd20a3b11b46522e3c705a0b6a72b9dc", "panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c", "panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0", "panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 71c4381108a..eda4a4821f0 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -10,7 +10,7 @@ .flex-1{-ms-flex:1 1 0.000000001px;-webkit-flex:1;flex:1;-webkit-flex-basis:0.000000001px;flex-basis:0.000000001px;}.flex-2{-ms-flex:2;-webkit-flex:2;flex:2;}.flex-3{-ms-flex:3;-webkit-flex:3;flex:3;}.flex-4{-ms-flex:4;-webkit-flex:4;flex:4;}.flex-5{-ms-flex:5;-webkit-flex:5;flex:5;}.flex-6{-ms-flex:6;-webkit-flex:6;flex:6;}.flex-7{-ms-flex:7;-webkit-flex:7;flex:7;}.flex-8{-ms-flex:8;-webkit-flex:8;flex:8;}.flex-9{-ms-flex:9;-webkit-flex:9;flex:9;}.flex-10{-ms-flex:10;-webkit-flex:10;flex:10;}.flex-11{-ms-flex:11;-webkit-flex:11;flex:11;}.flex-12{-ms-flex:12;-webkit-flex:12;flex:12;} \ No newline at end of file + ha-automation-editor{height:100%;} \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz index e794d373597..9c8b72d6bcc 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 21824a12326..a5544a8b165 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -37,7 +37,7 @@ /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ 'use strict'; -var precacheConfig = [["/","07ae53d16e9e97de8c721f5032cf79bf"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-5a2a3d6181cc820f5b3e94d1a50def74.html","6cd425233aeb180178dccae238533d65"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]]; +var precacheConfig = [["/","535d629ec4d3936dba0ca4ca84dabeb2"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-6c8192a4393c9e83516dc8177b75c23d.html","56d5bfe9e11a8b81a686f20aeae3c359"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]]; var cacheName = 'sw-precache-v3--' + (self.registration ? self.registration.scope : ''); diff --git a/homeassistant/components/frontend/www_static/service_worker.js.gz b/homeassistant/components/frontend/www_static/service_worker.js.gz index 980bc5d26a8..97665c7f31e 100644 Binary files a/homeassistant/components/frontend/www_static/service_worker.js.gz and b/homeassistant/components/frontend/www_static/service_worker.js.gz differ diff --git a/homeassistant/components/graphite.py b/homeassistant/components/graphite.py index fc2a196c4c6..e4626d0f016 100644 --- a/homeassistant/components/graphite.py +++ b/homeassistant/components/graphite.py @@ -45,13 +45,12 @@ def setup(hass, config): try: sock.connect((host, port)) sock.shutdown(2) - _LOGGER.debug('Connection to Graphite possible') + _LOGGER.debug("Connection to Graphite possible") except socket.error: - _LOGGER.error('Not able to connect to Graphite') + _LOGGER.error("Not able to connect to Graphite") return False GraphiteFeeder(hass, host, port, prefix) - return True @@ -143,15 +142,15 @@ class GraphiteFeeder(threading.Thread): _LOGGER.debug("Processing STATE_CHANGED event for %s", event.data['entity_id']) try: - self._report_attributes(event.data['entity_id'], - event.data['new_state']) + self._report_attributes( + event.data['entity_id'], event.data['new_state']) # pylint: disable=broad-except except Exception: # Catch this so we can avoid the thread dying and # make it visible. _LOGGER.exception("Failed to process STATE_CHANGED event") else: - _LOGGER.warning("Processing unexpected event type %s", - event.event_type) + _LOGGER.warning( + "Processing unexpected event type %s", event.event_type) self._queue.task_done() diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py index 1c999782ec7..6de85f022f3 100644 --- a/homeassistant/components/image_processing/dlib_face_detect.py +++ b/homeassistant/components/image_processing/dlib_face_detect.py @@ -15,7 +15,7 @@ from homeassistant.components.image_processing import ( from homeassistant.components.image_processing.microsoft_face_identify import ( ImageProcessingFaceEntity) -REQUIREMENTS = ['face_recognition==0.2.0'] +REQUIREMENTS = ['face_recognition==0.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/dlib_face_identify.py b/homeassistant/components/image_processing/dlib_face_identify.py index ec98d5bdcff..50a7bc846c4 100644 --- a/homeassistant/components/image_processing/dlib_face_identify.py +++ b/homeassistant/components/image_processing/dlib_face_identify.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import ( ImageProcessingFaceEntity) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['face_recognition==0.2.0'] +REQUIREMENTS = ['face_recognition==0.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 7cadbd0dd7f..92807bf9b1c 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -79,8 +79,12 @@ def async_setup(hass, config): # # Override the device default capabilities for a specific address # - plm.protocol.devices.add_override( - device['address'], 'capabilities', [device['platform']]) + if isinstance(device['platform'], list): + plm.protocol.devices.add_override( + device['address'], 'capabilities', device['platform']) + else: + plm.protocol.devices.add_override( + device['address'], 'capabilities', [device['platform']]) hass.data['insteon_plm'] = plm diff --git a/homeassistant/components/light/decora_wifi.py b/homeassistant/components/light/decora_wifi.py index 3a4b892cbfe..971ad21e84b 100644 --- a/homeassistant/components/light/decora_wifi.py +++ b/homeassistant/components/light/decora_wifi.py @@ -116,8 +116,8 @@ class DecoraWifiLight(Light): attribs = {'power': 'ON'} if ATTR_BRIGHTNESS in kwargs: - min_level = self._switch.get('minLevel', 0) - max_level = self._switch.get('maxLevel', 100) + min_level = self._switch.data.get('minLevel', 0) + max_level = self._switch.data.get('maxLevel', 100) brightness = int(kwargs[ATTR_BRIGHTNESS] * max_level / 255) brightness = max(brightness, min_level) attribs['brightness'] = brightness diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 21012f81658..209c3ab7724 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -122,7 +122,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ipaddr in light_ips: continue device['name'] = '{} {}'.format(device['id'], ipaddr) - device[ATTR_MODE] = 'rgbw' + device[ATTR_MODE] = MODE_RGBW device[CONF_PROTOCOL] = None light = FluxLight(device) lights.append(light) @@ -216,9 +216,9 @@ class FluxLight(Light): elif rgb is not None: self._bulb.setRgb(*tuple(rgb)) elif brightness is not None: - if self._mode == 'rgbw': + if self._mode == MODE_RGBW: self._bulb.setWarmWhite255(brightness) - elif self._mode == 'rgb': + elif self._mode == MODE_RGB: (red, green, blue) = self._bulb.getRgb() self._bulb.setRgb(red, green, blue, brightness=brightness) elif effect == EFFECT_RANDOM: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index cdbea7d2194..746c6489c9e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -23,7 +23,6 @@ from homeassistant.components.light import ( SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.config import load_yaml_config_file from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME) -from homeassistant.loader import get_component from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE import homeassistant.helpers.config_validation as cv @@ -164,9 +163,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable, # If we came here and configuring this host, mark as done if host in _CONFIGURING: request_id = _CONFIGURING.pop(host) - - configurator = get_component('configurator') - + configurator = hass.components.configurator configurator.request_done(request_id) lights = {} @@ -268,7 +265,7 @@ def request_configuration(host, hass, add_devices, filename, allow_unreachable, allow_in_emulated_hue, allow_hue_groups): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if host in _CONFIGURING: @@ -284,7 +281,7 @@ def request_configuration(host, hass, add_devices, filename, allow_in_emulated_hue, allow_hue_groups) _CONFIGURING[host] = configurator.request_config( - hass, "Philips Hue", hue_configuration_callback, + "Philips Hue", hue_configuration_callback, description=("Press the button on the bridge to register Philips Hue " "with Home Assistant."), entity_picture="/static/images/logo_philips_hue.png", @@ -384,7 +381,6 @@ class HueLight(Light): hue, sat = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) command['hue'] = hue command['sat'] = sat - command['bri'] = self.info['bri'] else: command['xy'] = kwargs[ATTR_XY_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -399,14 +395,13 @@ class HueLight(Light): *(int(val) for val in kwargs[ATTR_RGB_COLOR])) command['xy'] = xyb[0], xyb[1] command['bri'] = xyb[2] + elif ATTR_COLOR_TEMP in kwargs: + temp = kwargs[ATTR_COLOR_TEMP] + command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) if ATTR_BRIGHTNESS in kwargs: command['bri'] = kwargs[ATTR_BRIGHTNESS] - if ATTR_COLOR_TEMP in kwargs: - temp = kwargs[ATTR_COLOR_TEMP] - command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) - flash = kwargs.get(ATTR_FLASH) if flash == FLASH_LONG: @@ -425,9 +420,9 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif self.bridge_type == 'hue': - if self.info.get('manufacturername') != "OSRAM": - command['effect'] = 'none' + elif (self.bridge_type == 'hue' and + self.info.get('manufacturername') == 'Philips'): + command['effect'] = 'none' self._command_func(self.light_id, command) diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index e5b99ca1cb2..ebd6ab92d0f 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -11,7 +11,6 @@ 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 _CONFIGURING = {} @@ -54,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def request_configuration(device_id, insteonhub, model, hass, add_devices_callback): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if device_id in _CONFIGURING: @@ -69,7 +68,7 @@ def request_configuration(device_id, insteonhub, model, hass, add_devices_callback) _CONFIGURING[device_id] = configurator.request_config( - hass, 'Insteon ' + model + ' addr: ' + device_id, + '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', @@ -82,7 +81,7 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback): """Set up the light.""" if device_id in _CONFIGURING: request_id = _CONFIGURING.pop(device_id) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) _LOGGER.debug("Device configuration done") diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index a7301c24a88..6b57a1c5146 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -325,29 +325,33 @@ class LIFXManager(object): entity = self.entities[device.mac_addr] entity.registered = True _LOGGER.debug("%s register AGAIN", entity.who) - yield from entity.async_update() - yield from entity.async_update_ha_state() + yield from entity.update_hass() else: _LOGGER.debug("%s register NEW", device.ip_addr) - device.timeout = MESSAGE_TIMEOUT - device.retry_count = MESSAGE_RETRIES - device.unregister_timeout = UNAVAILABLE_GRACE + # Read initial state ack = AwaitAioLIFX().wait - yield from ack(device.get_version) - yield from ack(device.get_color) + version_resp = yield from ack(device.get_version) + if version_resp: + color_resp = yield from ack(device.get_color) - if lifxwhite(device): - entity = LIFXWhite(device, self.effects_conductor) - elif lifxmultizone(device): - yield from ack(partial(device.get_color_zones, start_index=0)) - entity = LIFXStrip(device, self.effects_conductor) + if version_resp is None or color_resp is None: + _LOGGER.error("Failed to initialize %s", device.ip_addr) else: - entity = LIFXColor(device, self.effects_conductor) + device.timeout = MESSAGE_TIMEOUT + device.retry_count = MESSAGE_RETRIES + device.unregister_timeout = UNAVAILABLE_GRACE - _LOGGER.debug("%s register READY", entity.who) - self.entities[device.mac_addr] = entity - self.async_add_devices([entity]) + if lifxwhite(device): + entity = LIFXWhite(device, self.effects_conductor) + elif lifxmultizone(device): + entity = LIFXStrip(device, self.effects_conductor) + else: + entity = LIFXColor(device, self.effects_conductor) + + _LOGGER.debug("%s register READY", entity.who) + self.entities[device.mac_addr] = entity + self.async_add_devices([entity], True) @callback def unregister(self, device): @@ -674,9 +678,14 @@ class LIFXStrip(LIFXColor): @asyncio.coroutine def update_color_zones(self): """Get updated color information for each zone.""" - ack = AwaitAioLIFX().wait - bulb = self.device - - # Each get_color_zones returns the next 8 zones - for zone in range(0, len(bulb.color_zones), 8): - yield from ack(partial(bulb.get_color_zones, start_index=zone)) + zone = 0 + top = 1 + while self.available and zone < top: + # Each get_color_zones can update 8 zones at once + resp = yield from AwaitAioLIFX().wait(partial( + self.device.get_color_zones, + start_index=zone, + end_index=zone+7)) + if resp: + zone += 8 + top = resp.count diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 203119e5e51..c41f480c67e 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,64 +4,35 @@ Support for MySensors lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mysensors/ """ -import logging - from homeassistant.components import mysensors from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list -_LOGGER = logging.getLogger(__name__) -ATTR_VALUE = 'value' -ATTR_VALUE_TYPE = 'value_type' - SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for lights.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DIMMER: [set_req.V_DIMMER], - } - device_class_map = { - pres.S_DIMMER: MySensorsLightDimmer, - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_RGB_LIGHT: [set_req.V_RGB], - pres.S_RGBW_LIGHT: [set_req.V_RGBW], - }) - map_sv_types[pres.S_DIMMER].append(set_req.V_PERCENTAGE) - device_class_map.update({ - pres.S_RGB_LIGHT: MySensorsLightRGB, - pres.S_RGBW_LIGHT: MySensorsLightRGBW, - }) - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, device_class_map, add_devices)) + """Setup the mysensors platform for lights.""" + device_class_map = { + 'S_DIMMER': MySensorsLightDimmer, + 'S_RGB_LIGHT': MySensorsLightRGB, + 'S_RGBW_LIGHT': MySensorsLightRGBW, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + add_devices=add_devices) -class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): +class MySensorsLight(mysensors.MySensorsEntity, Light): """Representation of a MySensors Light child node.""" def __init__(self, *args): """Initialize a MySensors Light.""" - mysensors.MySensorsDeviceEntity.__init__(self, *args) + super().__init__(*args) self._state = None self._brightness = None self._rgb = None @@ -101,7 +72,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): """Turn on light child device.""" set_req = self.gateway.const.SetReq - if self._state or set_req.V_LIGHT not in self._values: + if self._state: return self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 1) @@ -110,7 +81,6 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._state = True self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() def _turn_on_dimmer(self, **kwargs): """Turn on dimmer child device.""" @@ -130,7 +100,6 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._brightness = brightness self._values[set_req.V_DIMMER] = percent - self.schedule_update_ha_state() def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" @@ -144,16 +113,11 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): return if new_rgb is not None: rgb = list(new_rgb) - if rgb is None: - return if hex_template == '%02x%02x%02x%02x': if new_white is not None: rgb.append(new_white) - elif white is not None: - rgb.append(white) else: - _LOGGER.error("White value is not updated for RGBW light") - return + rgb.append(white) hex_color = hex_template % tuple(rgb) if len(rgb) > 3: white = rgb.pop() @@ -164,104 +128,40 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._rgb = rgb self._white = white - if hex_color: - self._values[self.value_type] = hex_color - self.schedule_update_ha_state() + self._values[self.value_type] = hex_color - def _turn_off_light(self, value_type=None, value=None): - """Turn off light child device.""" - set_req = self.gateway.const.SetReq - value_type = ( - set_req.V_LIGHT - if set_req.V_LIGHT in self._values else value_type) - value = 0 if set_req.V_LIGHT in self._values else value - return {ATTR_VALUE_TYPE: value_type, ATTR_VALUE: value} - - def _turn_off_dimmer(self, value_type=None, value=None): - """Turn off dimmer child device.""" - set_req = self.gateway.const.SetReq - value_type = ( - set_req.V_DIMMER - if set_req.V_DIMMER in self._values else value_type) - value = 0 if set_req.V_DIMMER in self._values else value - return {ATTR_VALUE_TYPE: value_type, ATTR_VALUE: value} - - def _turn_off_rgb_or_w(self, value_type=None, value=None): - """Turn off RGB or RGBW child device.""" - if float(self.gateway.protocol_version) >= 1.5: - set_req = self.gateway.const.SetReq - if self.value_type == set_req.V_RGB: - value = '000000' - elif self.value_type == set_req.V_RGBW: - value = '00000000' - return {ATTR_VALUE_TYPE: self.value_type, ATTR_VALUE: value} - - def _turn_off_main(self, value_type=None, value=None): + def turn_off(self): """Turn the device off.""" - set_req = self.gateway.const.SetReq - if value_type is None or value is None: - _LOGGER.warning( - "%s: value_type %s, value = %s, None is not valid argument " - "when setting child value", self._name, value_type, value) - return + value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value( - self.node_id, self.child_id, value_type, value) + self.node_id, self.child_id, value_type, 0) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False - self._values[value_type] = ( - STATE_OFF if set_req.V_LIGHT in self._values else value) + self._values[value_type] = STATE_OFF self.schedule_update_ha_state() def _update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT - if value_type in self._values: - self._values[value_type] = ( - STATE_ON if int(self._values[value_type]) == 1 else STATE_OFF) - self._state = self._values[value_type] == STATE_ON + self._state = self._values[value_type] == STATE_ON def _update_dimmer(self): """Update the controller with values from dimmer child.""" - set_req = self.gateway.const.SetReq - value_type = set_req.V_DIMMER + value_type = self.gateway.const.SetReq.V_DIMMER if value_type in self._values: self._brightness = round(255 * int(self._values[value_type]) / 100) if self._brightness == 0: self._state = False - if set_req.V_LIGHT not in self._values: - self._state = self._brightness > 0 def _update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" - set_req = self.gateway.const.SetReq value = self._values[self.value_type] - if len(value) != 6 and len(value) != 8: - _LOGGER.error( - "Wrong value %s for %s", value, set_req(self.value_type).name) - return color_list = rgb_hex_to_rgb_list(value) - if set_req.V_LIGHT not in self._values and \ - set_req.V_DIMMER not in self._values: - self._state = max(color_list) > 0 if len(color_list) > 3: - if set_req.V_RGBW != self.value_type: - _LOGGER.error( - "Wrong value %s for %s", - value, set_req(self.value_type).name) - return self._white = color_list.pop() self._rgb = color_list - def _update_main(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - self._values[value_type] = value - class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" @@ -270,18 +170,12 @@ class MySensorsLightDimmer(MySensorsLight): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) - - def turn_off(self, **kwargs): - """Turn the device off.""" - ret = self._turn_off_dimmer() - ret = self._turn_off_light( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - self._turn_off_main( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) + if self.gateway.optimistic: + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - self._update_main() + super().update() self._update_light() self._update_dimmer() @@ -294,20 +188,12 @@ class MySensorsLightRGB(MySensorsLight): self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) - - def turn_off(self, **kwargs): - """Turn the device off.""" - ret = self._turn_off_rgb_or_w() - ret = self._turn_off_dimmer( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - ret = self._turn_off_light( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - self._turn_off_main( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) + if self.gateway.optimistic: + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - self._update_main() + super().update() self._update_light() self._update_dimmer() self._update_rgb_or_w() @@ -316,8 +202,12 @@ class MySensorsLightRGB(MySensorsLight): class MySensorsLightRGBW(MySensorsLightRGB): """RGBW child class to MySensorsLightRGB.""" + # pylint: disable=too-many-ancestors + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) + if self.gateway.optimistic: + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index f2d39dea633..55b64bf8a74 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pwmled==1.1.1'] +REQUIREMENTS = ['pwmled==1.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index beca5fc6aec..14288b8848d 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -5,14 +5,17 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.tplink/ """ import logging +import colorsys from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR) from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin -from homeassistant.util.color import \ - color_temperature_kelvin_to_mired as kelvin_to_mired +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired as kelvin_to_mired) + +from typing import Tuple REQUIREMENTS = ['pyHS100==0.2.4.2'] @@ -39,10 +42,26 @@ def brightness_from_percentage(percent): return (percent*255.0)/100.0 +# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 +# pylint: disable=invalid-sequence-index +def rgb_to_hsv(rgb: Tuple[float, float, float]) -> Tuple[int, int, int]: + """Convert RGB tuple (values 0-255) to HSV (degrees, %, %).""" + hue, sat, value = colorsys.rgb_to_hsv(rgb[0]/255, rgb[1]/255, rgb[2]/255) + return int(hue * 360), int(sat * 100), int(value * 100) + + +# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 +# pylint: disable=invalid-sequence-index +def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: + """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" + red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) + return int(red * 255), int(green * 255), int(blue * 255) + + class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" - def __init__(self, smartbulb, name): + def __init__(self, smartbulb: 'SmartBulb', name): """Initialize the bulb.""" self.smartbulb = smartbulb @@ -55,6 +74,7 @@ class TPLinkSmartBulb(Light): self._state = None self._color_temp = None self._brightness = None + self._rgb = None _LOGGER.debug("Setting up TP-Link Smart Bulb") @property @@ -64,6 +84,8 @@ class TPLinkSmartBulb(Light): def turn_on(self, **kwargs): """Turn the light on.""" + self.smartbulb.state = self.smartbulb.BULB_STATE_ON + if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) @@ -72,7 +94,9 @@ class TPLinkSmartBulb(Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) self.smartbulb.brightness = brightness_to_percentage(brightness) - self.smartbulb.state = self.smartbulb.BULB_STATE_ON + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs.get(ATTR_RGB_COLOR) + self.smartbulb.hsv = rgb_to_hsv(rgb) def turn_off(self): """Turn the light off.""" @@ -88,6 +112,11 @@ class TPLinkSmartBulb(Light): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def rgb_color(self): + """Return the color in RGB.""" + return self._rgb + @property def is_on(self): """True if device is on.""" @@ -106,10 +135,14 @@ class TPLinkSmartBulb(Light): self.smartbulb.color_temp != 0): self._color_temp = kelvin_to_mired( self.smartbulb.color_temp) + self._rgb = hsv_to_rgb(self.smartbulb.hsv) except (SmartPlugException, OSError) as ex: _LOGGER.warning('Could not read state for %s: %s', self.name, ex) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_TPLINK + supported_features = SUPPORT_TPLINK + if self.smartbulb.is_color: + supported_features += SUPPORT_RGB_COLOR + return supported_features diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index e286bf330a1..1f7ee2ba5f9 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/light.yeelight/ """ import logging import colorsys +from typing import Tuple import voluptuous as vol @@ -89,6 +90,14 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] +# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 +# pylint: disable=invalid-sequence-index +def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: + """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" + red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) + return int(red * 255), int(green * 255), int(blue * 255) + + def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): @@ -192,10 +201,10 @@ class YeelightLight(Light): if color_mode == 2: # color temperature return color_temperature_to_rgb(self.color_temp) if color_mode == 3: # hsv - hue = self._properties.get('hue') - sat = self._properties.get('sat') - val = self._properties.get('bright') - return colorsys.hsv_to_rgb(hue, sat, val) + hue = int(self._properties.get('hue')) + sat = int(self._properties.get('sat')) + val = int(self._properties.get('bright')) + return hsv_to_rgb((hue, sat, val)) rgb = int(rgb) blue = rgb & 0xff @@ -214,7 +223,7 @@ class YeelightLight(Light): return self._bulb.last_properties @property - def _bulb(self) -> object: + def _bulb(self) -> 'yeelight.Bulb': import yeelight if self._bulb_device is None: try: diff --git a/homeassistant/components/lock/nello.py b/homeassistant/components/lock/nello.py new file mode 100644 index 00000000000..47a8e3146aa --- /dev/null +++ b/homeassistant/components/lock/nello.py @@ -0,0 +1,99 @@ +""" +Nello.io lock platform. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/lock.nello/ +""" +from itertools import filterfalse +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME) + +REQUIREMENTS = ['pynello==1.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_LOCATION_ID = 'location_id' +EVENT_DOOR_BELL = 'nello_bell_ring' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Nello lock platform.""" + from pynello import Nello + nello = Nello(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) + add_devices([NelloLock(lock) for lock in nello.locations], True) + + +class NelloLock(LockDevice): + """Representation of a Nello lock.""" + + def __init__(self, nello_lock): + """Initialize the lock.""" + self._nello_lock = nello_lock + self._device_attrs = None + self._activity = None + self._name = None + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return True + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return self._device_attrs + + def update(self): + """Update the nello lock properties.""" + self._nello_lock.update() + # Location identifiers + location_id = self._nello_lock.location_id + short_id = self._nello_lock.short_id + address = self._nello_lock.address + self._name = 'Nello {}'.format(short_id) + self._device_attrs = { + ATTR_ADDRESS: address, + ATTR_LOCATION_ID: location_id + } + # Process recent activity + activity = self._nello_lock.activity + if self._activity: + # Filter out old events + new_activity = list( + filterfalse(lambda x: x in self._activity, activity)) + if new_activity: + for act in new_activity: + activity_type = act.get('type') + if activity_type == 'bell.ring.denied': + event_data = { + 'address': address, + 'date': act.get('date'), + 'description': act.get('description'), + 'location_id': location_id, + 'short_id': short_id + } + self.hass.bus.fire(EVENT_DOOR_BELL, event_data) + # Save the activity history so that we don't trigger an event twice + self._activity = activity + + def unlock(self, **kwargs): + """Unlock the device.""" + if not self._nello_lock.open_door(): + _LOGGER.error("Failed to unlock") diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 691e11d01eb..f1d6139ffb1 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -15,7 +15,7 @@ from homeassistant.components.media_player import ( from homeassistant.config import load_yaml_config_file from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2017.8.6'] +REQUIREMENTS = ['youtube_dl==2017.8.18'] _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def setup(hass, config): 'media_player', 'services.yaml')) def play_media(call): - """Get stream URL and send it to the media_player.play_media.""" + """Get stream URL and send it to the play_media service.""" MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() hass.services.register(DOMAIN, @@ -66,7 +66,7 @@ class MEQueryException(Exception): pass -class MediaExtractor: +class MediaExtractor(object): """Class which encapsulates all extraction logic.""" def __init__(self, hass, component_config, call_data): @@ -107,15 +107,14 @@ class MediaExtractor: ydl = YoutubeDL({'quiet': True, 'logger': _LOGGER}) try: - all_media = ydl.extract_info(self.get_media_url(), - process=False) + all_media = ydl.extract_info(self.get_media_url(), process=False) except DownloadError: # This exception will be logged by youtube-dl itself raise MEDownloadException() if 'entries' in all_media: - _LOGGER.warning("Playlists are not supported, " - "looking for the first video") + _LOGGER.warning( + "Playlists are not supported, looking for the first video") entries = list(all_media['entries']) if len(entries) > 0: selected_media = entries[0] @@ -126,14 +125,14 @@ class MediaExtractor: selected_media = all_media def stream_selector(query): - """Find stream url that matches query.""" + """Find stream URL that matches query.""" try: ydl.params['format'] = query - requested_stream = ydl.process_ie_result(selected_media, - download=False) + requested_stream = ydl.process_ie_result( + selected_media, download=False) except (ExtractorError, DownloadError): - _LOGGER.error("Could not extract stream for the query: %s", - query) + _LOGGER.error( + "Could not extract stream for the query: %s", query) raise MEQueryException() return requested_stream['url'] @@ -141,7 +140,7 @@ class MediaExtractor: return stream_selector def call_media_player_service(self, stream_selector, entity_id): - """Call media_player.play_media service.""" + """Call Media player play_media service.""" stream_query = self.get_stream_query_for_entity(entity_id) try: @@ -164,8 +163,8 @@ class MediaExtractor: def get_stream_query_for_entity(self, entity_id): """Get stream format query for entity.""" - default_stream_query = self.config.get(CONF_DEFAULT_STREAM_QUERY, - DEFAULT_STREAM_QUERY) + default_stream_query = self.config.get( + CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY) if entity_id: media_content_type = self.call_data.get(ATTR_MEDIA_CONTENT_TYPE) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 93071b9840f..399052611c1 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -11,7 +11,6 @@ import re import voluptuous as vol -from homeassistant.loader import get_component from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, @@ -132,7 +131,7 @@ def setup_bravia(config, pin, hass, add_devices): # If we came here and configuring this host, mark as done if host in _CONFIGURING: request_id = _CONFIGURING.pop(host) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) _LOGGER.info("Discovery configuration done") @@ -150,7 +149,7 @@ def request_configuration(config, hass, add_devices): host = config.get(CONF_HOST) name = config.get(CONF_NAME) - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if host in _CONFIGURING: @@ -171,7 +170,7 @@ def request_configuration(config, hass, add_devices): request_configuration(config, hass, add_devices) _CONFIGURING[host] = configurator.request_config( - hass, name, bravia_configuration_callback, + name, bravia_configuration_callback, description='Enter the Pin shown on your Sony Bravia TV.' + 'If no Pin is shown, enter 0000 to let TV show you a Pin.', description_image="/static/images/smart-tv.png", diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 269964ea6c7..4090f420855 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -18,7 +18,6 @@ from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME) -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['websocket-client==0.37.0'] @@ -48,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def request_configuration(hass, config, url, add_devices_callback): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator if 'gpmdp' in _CONFIGURING: configurator.notify_errors( _CONFIGURING['gpmdp'], "Failed to register, please try again.") @@ -96,7 +95,7 @@ def request_configuration(hass, config, url, add_devices_callback): break _CONFIGURING['gpmdp'] = configurator.request_config( - hass, DEFAULT_NAME, gpmdp_configuration_callback, + DEFAULT_NAME, gpmdp_configuration_callback, description=( 'Enter the pin that is displayed in the ' 'Google Play Music Desktop Player.'), @@ -117,7 +116,7 @@ def setup_gpmdp(hass, config, code, add_devices): return if 'gpmdp' in _CONFIGURING: - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(_CONFIGURING.pop('gpmdp')) add_devices([GPMDP(name, url, code)], True) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 793588a8d9f..55df1e367a4 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -79,14 +79,15 @@ class MpdDevice(MediaPlayerDevice): self._client = mpd.MPDClient() self._client.timeout = 5 self._client.idletimeout = None - if password is not None: - self._client.password(password) def _connect(self): """Connect to MPD.""" import mpd try: self._client.connect(self.server, self.port) + + if self.password is not None: + self._client.password(self.password) except mpd.ConnectionError: return diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 927de799ae9..97ebe5be92b 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['onkyo-eiscp==1.1'] +REQUIREMENTS = ['onkyo-eiscp==1.2.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index f4c69ba1fe6..a901cd1d569 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -23,7 +23,6 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change -from homeassistant.loader import get_component REQUIREMENTS = ['plexapi==2.0.2'] @@ -143,7 +142,7 @@ def setup_plexserver( # If we came here and configuring this host, mark as done if host in _CONFIGURING: request_id = _CONFIGURING.pop(host) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) _LOGGER.info("Discovery configuration done") @@ -236,7 +235,7 @@ def setup_plexserver( def request_configuration(host, hass, config, add_devices_callback): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if host in _CONFIGURING: configurator.notify_errors(_CONFIGURING[host], @@ -254,7 +253,6 @@ def request_configuration(host, hass, config, add_devices_callback): ) _CONFIGURING[host] = configurator.request_config( - hass, 'Plex Media Server', plex_configuration_callback, description=('Enter the X-Plex-Token'), diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 7b0036e5f96..1715f0f1829 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -20,7 +20,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file -REQUIREMENTS = ['snapcast==2.0.6'] +REQUIREMENTS = ['snapcast==2.0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index bc0728c7ff2..239b13a6292 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -10,7 +10,6 @@ from datetime import timedelta import voluptuous as vol from homeassistant.core import callback -from homeassistant.loader import get_component from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET, @@ -62,9 +61,9 @@ SCAN_INTERVAL = timedelta(seconds=30) def request_configuration(hass, config, add_devices, oauth): """Request Spotify authorization.""" - configurator = get_component('configurator') + configurator = hass.components.configurator hass.data[DOMAIN] = configurator.request_config( - hass, DEFAULT_NAME, lambda _: None, + DEFAULT_NAME, lambda _: None, link_name=CONFIGURATOR_LINK_NAME, link_url=oauth.get_authorize_url(), description=CONFIGURATOR_DESCRIPTION, @@ -88,7 +87,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): request_configuration(hass, config, add_devices, oauth) return if hass.data.get(DOMAIN): - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(hass.data.get(DOMAIN)) del hass.data[DOMAIN] player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME), @@ -186,7 +185,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._artist = ', '.join([artist.get('name') for artist in item.get('artists')]) self._uri = current.get('uri') - self._image_url = item.get('album').get('images')[0].get('url') + images = item.get('album').get('images') + self._image_url = images[0].get('url') if images else None # Playing state self._state = STATE_PAUSED if current.get('is_playing'): diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 859d596f98f..a4a15fbce24 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -95,7 +95,8 @@ class LogitechMediaServer(object): """Create a list of devices connected to LMS.""" result = [] data = yield from self.async_query('players', 'status') - + if data is False: + return result for players in data.get('players_loop', []): player = SqueezeBoxDevice( self, players['playerid'], players['name']) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 112b84ec5f0..65a999528c3 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -19,10 +19,9 @@ from homeassistant.components.media_player import ( SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( - CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, STATE_OFF, + CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, CONF_TIMEOUT, STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN, CONF_NAME, CONF_FILENAME) -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pylgtv==0.1.7', @@ -56,7 +55,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string + vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, + vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int, }) @@ -79,17 +79,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mac = config.get(CONF_MAC) name = config.get(CONF_NAME) customize = config.get(CONF_CUSTOMIZE) + timeout = config.get(CONF_TIMEOUT) config = hass.config.path(config.get(CONF_FILENAME)) - setup_tv(host, mac, name, customize, config, hass, add_devices) + setup_tv(host, mac, name, customize, config, timeout, hass, add_devices) -def setup_tv(host, mac, name, customize, config, hass, add_devices): +def setup_tv(host, mac, name, customize, config, timeout, hass, add_devices): """Set up a LG WebOS TV based on host parameter.""" from pylgtv import WebOsClient from pylgtv import PyLGTVPairException from websockets.exceptions import ConnectionClosed - client = WebOsClient(host, config) + client = WebOsClient(host, config, timeout) if not client.is_registered(): if host in _CONFIGURING: @@ -100,30 +101,30 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices): _LOGGER.warning( "Connected to LG webOS TV %s but not paired", host) return - except (OSError, ConnectionClosed, TypeError, - asyncio.TimeoutError): + except (OSError, ConnectionClosed, asyncio.TimeoutError): _LOGGER.error("Unable to connect to host %s", host) return else: # Not registered, request configuration. _LOGGER.warning("LG webOS TV %s needs to be paired", host) request_configuration( - host, mac, name, customize, config, hass, add_devices) + host, mac, name, customize, config, timeout, hass, add_devices) return # If we came here and configuring this host, mark as done. if client.is_registered() and host in _CONFIGURING: request_id = _CONFIGURING.pop(host) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) - add_devices([LgWebOSDevice(host, mac, name, customize, config)], True) + add_devices([LgWebOSDevice(host, mac, name, customize, config, timeout)], + True) def request_configuration( - host, mac, name, customize, config, hass, add_devices): + host, mac, name, customize, config, timeout, hass, add_devices): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if host in _CONFIGURING: @@ -133,11 +134,12 @@ def request_configuration( # pylint: disable=unused-argument def lgtv_configuration_callback(data): - """Handle configuration changes.""" - setup_tv(host, mac, name, customize, config, hass, add_devices) + """The actions to do when our configuration callback is called.""" + setup_tv(host, mac, name, customize, config, timeout, hass, + add_devices) _CONFIGURING[host] = configurator.request_config( - hass, name, lgtv_configuration_callback, + name, lgtv_configuration_callback, description='Click start and accept the pairing request on your TV.', description_image='/static/images/config_webos.png', submit_caption='Start pairing request' @@ -147,11 +149,11 @@ def request_configuration( class LgWebOSDevice(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, mac, name, customize, config): + def __init__(self, host, mac, name, customize, config, timeout): """Initialize the webos device.""" from pylgtv import WebOsClient from wakeonlan import wol - self._client = WebOsClient(host, config) + self._client = WebOsClient(host, config, timeout) self._wol = wol self._mac = mac self._customize = customize diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index ef863bfb34f..63bd1f6faac 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,30 +4,37 @@ Connect to a MySensors gateway via pymysensors API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ +import asyncio +from collections import defaultdict import logging import os import socket import sys +from timeit import default_timer as timer import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.setup import setup_component from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component +from homeassistant.setup import setup_component -REQUIREMENTS = ['pymysensors==0.10.0'] +REQUIREMENTS = ['pymysensors==0.11.0'] _LOGGER = logging.getLogger(__name__) ATTR_CHILD_ID = 'child_id' ATTR_DESCRIPTION = 'description' ATTR_DEVICE = 'device' +ATTR_DEVICES = 'devices' ATTR_NODE_ID = 'node_id' CONF_BAUD_RATE = 'baud_rate' @@ -44,11 +51,16 @@ CONF_VERSION = 'version' DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 -DEFAULT_VERSION = 1.4 +DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' +MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +PLATFORM = 'platform' +SCHEMA = 'schema' +SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' +TYPE = 'type' def is_socket_address(value): @@ -144,11 +156,127 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.Coerce(float), + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, })) }, extra=vol.ALLOW_EXTRA) +# mysensors const schemas +BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} +CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} +LIGHT_DIMMER_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_DIMMER', + SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} +LIGHT_PERCENTAGE_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_PERCENTAGE', + SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGB_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { + 'V_RGB': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGBW_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { + 'V_RGBW': cv.string, 'V_STATUS': cv.string}} +NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} +DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} +DUST_SCHEMA = [ + {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] +SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} +SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} +MYSENSORS_CONST_SCHEMA = { + 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SPRINKLER': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_WATER_LEAK': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SOUND': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_VIBRATION': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOISTURE': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_HVAC': [CLIMATE_SCHEMA], + 'S_COVER': [ + {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, + {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, + {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, + {PLATFORM: 'cover', TYPE: 'V_STATUS'}], + 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], + 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], + 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], + 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], + 'S_GPS': [ + DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], + 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], + 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], + 'S_BARO': [ + {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, + {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], + 'S_WIND': [ + {PLATFORM: 'sensor', TYPE: 'V_WIND'}, + {PLATFORM: 'sensor', TYPE: 'V_GUST'}, + {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], + 'S_RAIN': [ + {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, + {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], + 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], + 'S_WEIGHT': [ + {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_POWER': [ + {PLATFORM: 'sensor', TYPE: 'V_WATT'}, + {PLATFORM: 'sensor', TYPE: 'V_KWH'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR'}, + {PLATFORM: 'sensor', TYPE: 'V_VA'}, + {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], + 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], + 'S_LIGHT_LEVEL': [ + {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], + 'S_IR': [ + {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, + {PLATFORM: 'switch', TYPE: 'V_IR_SEND', + SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], + 'S_WATER': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_CUSTOM': [ + {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, + {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], + 'S_SCENE_CONTROLLER': [ + {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, + {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], + 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], + 'S_MULTIMETER': [ + {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, + {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_GAS': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_WATER_QUALITY': [ + {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, + {PLATFORM: 'sensor', TYPE: 'V_PH'}, + {PLATFORM: 'sensor', TYPE: 'V_ORP'}, + {PLATFORM: 'sensor', TYPE: 'V_EC'}, + {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_AIR_QUALITY': DUST_SCHEMA, + 'S_DUST': DUST_SCHEMA, + 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], + 'S_BINARY': [SWITCH_STATUS_SCHEMA], + 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], +} + + def setup(hass, config): """Set up the MySensors component.""" import mysensors.mysensors as mysensors @@ -197,20 +325,14 @@ def setup(hass, config): # invalid ip address return gateway.metric = hass.config.units.is_metric - optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) - gateway = GatewayWrapper(gateway, optimistic, device) - # pylint: disable=attribute-defined-outside-init - gateway.event_callback = gateway.callback_factory() + gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) + gateway.device = device + gateway.event_callback = gw_callback_factory(hass) def gw_start(event): """Trigger to start of the gateway and any persistence.""" if persistence: - for node_id in gateway.sensors: - node = gateway.sensors[node_id] - for child_id in node.children: - msg = mysensors.Message().modify( - node_id=node_id, child_id=child_id) - gateway.event_callback(msg) + discover_persistent_devices(hass, gateway) gateway.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) @@ -219,15 +341,8 @@ def setup(hass, config): return gateway - gateways = hass.data.get(MYSENSORS_GATEWAYS) - if gateways is not None: - _LOGGER.error( - "%s already exists in %s, will not setup %s component", - MYSENSORS_GATEWAYS, hass.data, DOMAIN) - return False - # Setup all devices from config - gateways = [] + gateways = {} conf_gateways = config[DOMAIN][CONF_GATEWAYS] for index, gway in enumerate(conf_gateways): @@ -243,7 +358,7 @@ def setup(hass, config): device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) if ready_gateway is not None: - gateways.append(ready_gateway) + gateways[id(ready_gateway)] = ready_gateway if not gateways: _LOGGER.error( @@ -252,115 +367,187 @@ def setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways - for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate', - 'cover']: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - discovery.load_platform( - hass, 'device_tracker', DOMAIN, {}, config) - - discovery.load_platform( - hass, 'notify', DOMAIN, {CONF_NAME: DOMAIN}, config) - return True -def pf_callback_factory(map_sv_types, devices, entity_class, add_devices=None): - """Return a new callback for the platform.""" - def mysensors_callback(gateway, msg): - """Run when a message from the gateway arrives.""" - if gateway.sensors[msg.node_id].sketch_name is None: - _LOGGER.debug("No sketch_name: node %s", msg.node_id) - return - child = gateway.sensors[msg.node_id].children.get(msg.child_id) +def validate_child(gateway, node_id, child): + """Validate that a child has the correct values according to schema. + + Return a dict of platform with a list of device ids for validated devices. + """ + validated = defaultdict(list) + + if not child.values: + _LOGGER.debug( + "No child values for node %s child %s", node_id, child.id) + return validated + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.debug("Node %s is missing sketch name", node_id) + return validated + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + s_name = next( + (member.name for member in pres if member.value == child.type), None) + if s_name not in MYSENSORS_CONST_SCHEMA: + _LOGGER.warning("Child type %s is not supported", s_name) + return validated + child_schemas = MYSENSORS_CONST_SCHEMA[s_name] + + def msg(name): + """Return a message for an invalid schema.""" + return "{} requires value_type {}".format( + pres(child.type).name, set_req[name].name) + + for schema in child_schemas: + platform = schema[PLATFORM] + v_name = schema[TYPE] + value_type = next( + (member.value for member in set_req if member.name == v_name), + None) + if value_type is None: + continue + _child_schema = child.get_schema(gateway.protocol_version) + vol_schema = _child_schema.extend( + {vol.Required(set_req[key].value, msg=msg(key)): + _child_schema.schema.get(set_req[key].value, val) + for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, + extra=vol.ALLOW_EXTRA) + try: + vol_schema(child.values) + except vol.Invalid as exc: + level = (logging.WARNING if value_type in child.values + else logging.DEBUG) + _LOGGER.log( + level, + "Invalid values: %s: %s platform: node %s child %s: %s", + child.values, platform, node_id, child.id, exc) + continue + dev_id = id(gateway), node_id, child.id, value_type + validated[platform].append(dev_id) + return validated + + +def discover_mysensors_platform(hass, platform, new_devices): + """Discover a mysensors platform.""" + discovery.load_platform( + hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) + + +def discover_persistent_devices(hass, gateway): + """Discover platforms for devices loaded via persistence file.""" + new_devices = defaultdict(list) + for node_id in gateway.sensors: + node = gateway.sensors[node_id] + for child in node.children.values(): + validated = validate_child(gateway, node_id, child) + for platform, dev_ids in validated.items(): + new_devices[platform].extend(dev_ids) + for platform, dev_ids in new_devices.items(): + discover_mysensors_platform(hass, platform, dev_ids) + + +def get_mysensors_devices(hass, domain): + """Return mysensors devices for a platform.""" + if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: + hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} + return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] + + +def gw_callback_factory(hass): + """Return a new callback for the gateway.""" + def mysensors_callback(msg): + """Default callback for a mysensors gateway.""" + start = timer() + _LOGGER.debug( + "Node update: node %s child %s", msg.node_id, msg.child_id) + + child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) if child is None: + _LOGGER.debug( + "Not a child update for node %s", msg.node_id) return - for value_type in child.values: - key = msg.node_id, child.id, value_type - if child.type not in map_sv_types or \ - value_type not in map_sv_types[child.type]: - continue - if key in devices: - if add_devices: - devices[key].schedule_update_ha_state(True) - else: - devices[key].update() - continue - name = '{} {} {}'.format( - gateway.sensors[msg.node_id].sketch_name, msg.node_id, - child.id) - if isinstance(entity_class, dict): - device_class = entity_class[child.type] - else: - device_class = entity_class - devices[key] = device_class( - gateway, msg.node_id, child.id, name, value_type) - if add_devices: - _LOGGER.info("Adding new devices: %s", [devices[key]]) - add_devices([devices[key]], True) - else: - devices[key].update() + + signals = [] + + # Update all platforms for the device via dispatcher. + # Add/update entity if schema validates to true. + validated = validate_child(msg.gateway, msg.node_id, child) + for platform, dev_ids in validated.items(): + devices = get_mysensors_devices(hass, platform) + for idx, dev_id in enumerate(list(dev_ids)): + if dev_id in devices: + dev_ids.pop(idx) + signals.append(SIGNAL_CALLBACK.format(*dev_id)) + if dev_ids: + discover_mysensors_platform(hass, platform, dev_ids) + for signal in set(signals): + # Only one signal per device is needed. + # A device can have multiple platforms, ie multiple schemas. + # FOR LATER: Add timer to not signal if another update comes in. + dispatcher_send(hass, signal) + end = timer() + if end - start > 0.1: + _LOGGER.debug( + "Callback for node %s child %s took %.3f seconds", + msg.node_id, msg.child_id, end - start) return mysensors_callback -class GatewayWrapper(object): - """Gateway wrapper class.""" - - def __init__(self, gateway, optimistic, device): - """Set up the class attributes on instantiation. - - Args: - gateway (mysensors.SerialGateway): Gateway to wrap. - optimistic (bool): Send values to actuators without feedback state. - device (str): Path to serial port, ip adress or mqtt. - - Attributes: - _wrapped_gateway (mysensors.SerialGateway): Wrapped gateway. - platform_callbacks (list): Callback functions, one per platform. - optimistic (bool): Send values to actuators without feedback state. - device (str): Device configured as gateway. - __initialised (bool): True if GatewayWrapper is initialised. - - """ - self._wrapped_gateway = gateway - self.platform_callbacks = [] - self.optimistic = optimistic - self.device = device - self.__initialised = True - - def __getattr__(self, name): - """See if this object has attribute name.""" - # Do not use hasattr, it goes into infinite recurrsion - if name in self.__dict__: - # This object has the attribute. - return getattr(self, name) - # The wrapped object has the attribute. - return getattr(self._wrapped_gateway, name) - - def __setattr__(self, name, value): - """See if this object has attribute name then set to value.""" - if '_GatewayWrapper__initialised' not in self.__dict__: - return object.__setattr__(self, name, value) - elif name in self.__dict__: - object.__setattr__(self, name, value) - else: - object.__setattr__(self._wrapped_gateway, name, value) - - def callback_factory(self): - """Return a new callback function.""" - def node_update(msg): - """Handle node updates from the MySensors gateway.""" - _LOGGER.debug( - "Update: node %s, child %s sub_type %s", - msg.node_id, msg.child_id, msg.sub_type) - for callback in self.platform_callbacks: - callback(self, msg) - - return node_update +def get_mysensors_name(gateway, node_id, child_id): + """Return a name for a node child.""" + return '{} {} {}'.format( + gateway.sensors[node_id].sketch_name, node_id, child_id) -class MySensorsDeviceEntity(object): - """Representation of a MySensors entity.""" +def get_mysensors_gateway(hass, gateway_id): + """Return gateway.""" + if MYSENSORS_GATEWAYS not in hass.data: + hass.data[MYSENSORS_GATEWAYS] = {} + gateways = hass.data.get(MYSENSORS_GATEWAYS) + return gateways.get(gateway_id) + + +def setup_mysensors_platform( + hass, domain, discovery_info, device_class, device_args=None, + add_devices=None): + """Set up a mysensors platform.""" + # Only act if called via mysensors by discovery event. + # Otherwise gateway is not setup. + if not discovery_info: + return + if device_args is None: + device_args = () + new_devices = [] + new_dev_ids = discovery_info[ATTR_DEVICES] + for dev_id in new_dev_ids: + devices = get_mysensors_devices(hass, domain) + if dev_id in devices: + continue + gateway_id, node_id, child_id, value_type = dev_id + gateway = get_mysensors_gateway(hass, gateway_id) + if not gateway: + continue + device_class_copy = device_class + if isinstance(device_class, dict): + child = gateway.sensors[node_id].children[child_id] + s_type = gateway.const.Presentation(child.type).name + device_class_copy = device_class[s_type] + name = get_mysensors_name(gateway, node_id, child_id) + + # python 3.4 cannot unpack inside tuple, but combining tuples works + args_copy = device_args + ( + gateway, node_id, child_id, name, value_type) + devices[dev_id] = device_class_copy(*args_copy) + new_devices.append(devices[dev_id]) + if new_devices: + _LOGGER.info("Adding new devices: %s", new_devices) + if add_devices is not None: + add_devices(new_devices, True) + return new_devices + + +class MySensorsDevice(object): + """Representation of a MySensors device.""" def __init__(self, gateway, node_id, child_id, name, value_type): """Set up the MySensors device.""" @@ -373,11 +560,6 @@ class MySensorsDeviceEntity(object): self.child_type = child.type self._values = {} - @property - def should_poll(self): - """Mysensor gateway pushes its state to HA.""" - return False - @property def name(self): """Return the name of this entity.""" @@ -399,18 +581,9 @@ class MySensorsDeviceEntity(object): set_req = self.gateway.const.SetReq for value_type, value in self._values.items(): - try: - attr[set_req(value_type).name] = value - except ValueError: - _LOGGER.error("Value_type %s is not valid for mysensors " - "version %s", value_type, - self.gateway.protocol_version) - return attr + attr[set_req(value_type).name] = value - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values + return attr def update(self): """Update the controller with the latest value from a sensor.""" @@ -419,7 +592,8 @@ class MySensorsDeviceEntity(object): set_req = self.gateway.const.SetReq for value_type, value in child.values.items(): _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) + "Entity update: %s: value_type %s, value = %s", + self._name, value_type, value) if value_type in (set_req.V_ARMED, set_req.V_LIGHT, set_req.V_LOCK_STATUS, set_req.V_TRIPPED): self._values[value_type] = ( @@ -428,3 +602,29 @@ class MySensorsDeviceEntity(object): self._values[value_type] = int(value) else: self._values[value_type] = value + + +class MySensorsEntity(MySensorsDevice, Entity): + """Representation of a MySensors entity.""" + + @property + def should_poll(self): + """Mysensor gateway pushes its state to HA.""" + return False + + @property + def available(self): + """Return true if entity is available.""" + return self.value_type in self._values + + def _async_update_callback(self): + """Update the entity.""" + self.hass.async_add_job(self.async_update_ha_state(True)) + + @asyncio.coroutine + def async_added_to_hass(self): + """Register update callback.""" + dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type + async_dispatcher_connect( + self.hass, SIGNAL_CALLBACK.format(*dev_id), + self._async_update_callback) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 6443fc47a85..512819b7e74 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -14,7 +14,6 @@ from homeassistant.helpers import discovery from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -from homeassistant.loader import get_component REQUIREMENTS = ['python-nest==3.1.0'] @@ -54,7 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ def request_configuration(nest, hass, config): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator if 'nest' in _CONFIGURING: _LOGGER.debug("configurator failed") configurator.notify_errors( @@ -68,7 +67,7 @@ def request_configuration(nest, hass, config): setup_nest(hass, nest, config, pin=pin) _CONFIGURING['nest'] = configurator.request_config( - hass, "Nest", nest_configuration_callback, + "Nest", nest_configuration_callback, description=('To configure Nest, click Request Authorization below, ' 'log into your Nest account, ' 'and then enter the resulting PIN'), @@ -92,7 +91,7 @@ def setup_nest(hass, nest, config, pin=None): if 'nest' in _CONFIGURING: _LOGGER.debug("configuration done") - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(_CONFIGURING.pop('nest')) _LOGGER.debug("proceeding with setup") diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index 691ff158012..a4ce304167f 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -4,16 +4,18 @@ Discord platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.discord/ """ -import logging import asyncio +import logging + import voluptuous as vol + import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['discord.py==0.16.8'] +REQUIREMENTS = ['discord.py==0.16.10'] CONF_TOKEN = 'token' @@ -42,13 +44,22 @@ class DiscordNotificationService(BaseNotificationService): import discord discord_bot = discord.Client(loop=self.hass.loop) + if ATTR_TARGET not in kwargs: + _LOGGER.error("No target specified") + return None + + # pylint: disable=unused-variable @discord_bot.event @asyncio.coroutine - def on_ready(): # pylint: disable=unused-variable + def on_ready(): """Send the messages when the bot is ready.""" - for channelid in kwargs[ATTR_TARGET]: - channel = discord.Object(id=channelid) - yield from discord_bot.send_message(channel, message) + try: + for channelid in kwargs[ATTR_TARGET]: + channel = discord.Object(id=channelid) + yield from discord_bot.send_message(channel, message) + except (discord.errors.HTTPException, + discord.errors.NotFound) as error: + _LOGGER.warning("Communication error: %s", error) yield from discord_bot.logout() yield from discord_bot.close() diff --git a/homeassistant/components/notify/hipchat.py b/homeassistant/components/notify/hipchat.py new file mode 100644 index 00000000000..ee1283b9820 --- /dev/null +++ b/homeassistant/components/notify/hipchat.py @@ -0,0 +1,97 @@ +""" +HipChat platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.hipchat/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_TOKEN, CONF_HOST + +REQUIREMENTS = ['hipnotify==1.0.8'] + +_LOGGER = logging.getLogger(__name__) + +CONF_COLOR = 'color' +CONF_ROOM = 'room' +CONF_NOTIFY = 'notify' +CONF_FORMAT = 'format' + +DEFAULT_COLOR = 'yellow' +DEFAULT_FORMAT = 'text' +DEFAULT_HOST = 'https://api.hipchat.com/' +DEFAULT_NOTIFY = False + +VALID_COLORS = {'yellow', 'green', 'red', 'purple', 'gray', 'random'} +VALID_FORMATS = {'text', 'html'} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ROOM): vol.Coerce(int), + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), + vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the HipChat notification service.""" + return HipchatNotificationService( + config[CONF_TOKEN], config[CONF_ROOM], config[CONF_COLOR], + config[CONF_NOTIFY], config[CONF_FORMAT], config[CONF_HOST]) + + +class HipchatNotificationService(BaseNotificationService): + """Implement the notification service for HipChat.""" + + def __init__(self, token, default_room, default_color, default_notify, + default_format, host): + """Initialize the service.""" + self._token = token + self._default_room = default_room + self._default_color = default_color + self._default_notify = default_notify + self._default_format = default_format + self._host = host + + self._rooms = {} + self._get_room(self._default_room) + + def _get_room(self, room): + """Get Room object, creating it if necessary.""" + from hipnotify import Room + if room not in self._rooms: + self._rooms[room] = Room( + token=self._token, room_id=room, endpoint_url=self._host) + return self._rooms[room] + + def send_message(self, message="", **kwargs): + """Send a message.""" + color = self._default_color + notify = self._default_notify + message_format = self._default_format + + if kwargs.get(ATTR_DATA) is not None: + data = kwargs.get(ATTR_DATA) + if ((data.get(CONF_COLOR) is not None) + and (data.get(CONF_COLOR) in VALID_COLORS)): + color = data.get(CONF_COLOR) + if ((data.get(CONF_NOTIFY) is not None) + and isinstance(data.get(CONF_NOTIFY), bool)): + notify = data.get(CONF_NOTIFY) + if ((data.get(CONF_FORMAT) is not None) + and (data.get(CONF_FORMAT) in VALID_FORMATS)): + message_format = data.get(CONF_FORMAT) + + targets = kwargs.get(ATTR_TARGET, [self._default_room]) + + for target in targets: + room = self._get_room(target) + room.notify(msg=message, color=color, notify=notify, + message_format=message_format) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index d9576767f25..8ae697048f5 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -6,35 +6,19 @@ https://home-assistant.io/components/notify.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) + ATTR_TARGET, DOMAIN, BaseNotificationService) def get_service(hass, config, discovery_info=None): """Get the MySensors notification service.""" - if discovery_info is None: + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsNotificationDevice) + if not new_devices: return - platform_devices = [] - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - if float(gateway.protocol_version) < 2.0: - continue - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_INFO: [set_req.V_TEXT], - } - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsNotificationDevice)) - platform_devices.append(devices) - - return MySensorsNotificationService(platform_devices) + return MySensorsNotificationService(hass) -class MySensorsNotificationDevice(mysensors.MySensorsDeviceEntity): +class MySensorsNotificationDevice(mysensors.MySensorsDevice): """Represent a MySensors Notification device.""" def send_msg(self, msg): @@ -44,24 +28,25 @@ class MySensorsNotificationDevice(mysensors.MySensorsDeviceEntity): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, sub_msg) + def __repr__(self): + """Return the representation.""" + return "".format(self.name) + class MySensorsNotificationService(BaseNotificationService): - """Implement MySensors notification service.""" + """Implement a MySensors notification service.""" # pylint: disable=too-few-public-methods - def __init__(self, platform_devices): + def __init__(self, hass): """Initialize the service.""" - self.platform_devices = platform_devices + self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) def send_message(self, message="", **kwargs): """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) - devices = [] - for gw_devs in self.platform_devices: - for device in gw_devs.values(): - if target_devices is None or device.name in target_devices: - devices.append(device) + devices = [device for device in self.devices.values() + if target_devices is None or device.name in target_devices] for device in devices: device.send_msg(message) diff --git a/homeassistant/components/notify/prowl.py b/homeassistant/components/notify/prowl.py new file mode 100644 index 00000000000..1298657a69a --- /dev/null +++ b/homeassistant/components/notify/prowl.py @@ -0,0 +1,70 @@ +""" +Prowl notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.prowl/ +""" +import logging +import asyncio + +import async_timeout +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://api.prowlapp.com/publicapi/' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, +}) + + +@asyncio.coroutine +def async_get_service(hass, config, discovery_info=None): + """Get the Prowl notification service.""" + return ProwlNotificationService(hass, config[CONF_API_KEY]) + + +class ProwlNotificationService(BaseNotificationService): + """Implement the notification service for Prowl.""" + + def __init__(self, hass, api_key): + """Initialize the service.""" + self._hass = hass + self._api_key = api_key + + @asyncio.coroutine + def async_send_message(self, message, **kwargs): + """Send the message to the user.""" + response = None + session = None + url = '{}{}'.format(_RESOURCE, 'add') + data = kwargs.get(ATTR_DATA) + payload = { + 'apikey': self._api_key, + 'application': 'Home-Assistant', + 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + 'description': message, + 'priority': data['priority'] if data and 'priority' in data else 0 + } + + _LOGGER.debug("Attempting call Prowl service at %s", url) + session = async_get_clientsession(self._hass) + + try: + with async_timeout.timeout(10, loop=self._hass.loop): + response = yield from session.post(url, data=payload) + result = yield from response.text() + + if response.status != 200 or 'error' in result: + _LOGGER.error("Prowl service returned http " + "status %d, response %s", + response.status, result) + except asyncio.TimeoutError: + _LOGGER.error("Timeout accessing Prowl at %s", url) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 8ac2bd06dad..6d97d544905 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -89,15 +89,7 @@ class PushBulletNotificationService(BaseNotificationService): if not targets: # Backward compatibility, notify all devices in own account - if url: - self.pushbullet.push_link(title, url, body=message) - if filepath and self.hass.config.is_allowed_path(filepath): - with open(filepath, "rb") as fileh: - filedata = self.pushbullet.upload_file(fileh, filepath) - self.pushbullet.push_file(title=title, body=message, - **filedata) - else: - self.pushbullet.push_note(title, message) + self._push_data(filepath, message, title, url) _LOGGER.info("Sent notification to self") return @@ -112,16 +104,7 @@ class PushBulletNotificationService(BaseNotificationService): # Target is email, send directly, don't use a target object # This also seems works to send to all devices in own account if ttype == 'email': - if url: - self.pushbullet.push_link( - title, url, body=message, email=tname) - if filepath and self.hass.config.is_allowed_path(filepath): - with open(filepath, "rb") as fileh: - filedata = self.pushbullet.upload_file(fileh, filepath) - self.pushbullet.push_file(title=title, body=message, - **filedata) - else: - self.pushbullet.push_note(title, message, email=tname) + self._push_data(filepath, message, title, url, tname) _LOGGER.info("Sent notification to email %s", tname) continue @@ -152,3 +135,15 @@ class PushBulletNotificationService(BaseNotificationService): except self.pushbullet.errors.PushError: _LOGGER.error("Notify failed to: %s/%s", ttype, tname) continue + + def _push_data(self, filepath, message, title, url, tname=None): + if url: + self.pushbullet.push_link( + title, url, body=message, email=tname) + elif filepath and self.hass.config.is_allowed_path(filepath): + with open(filepath, "rb") as fileh: + filedata = self.pushbullet.upload_file(fileh, filepath) + self.pushbullet.push_file(title=title, body=message, + **filedata) + else: + self.pushbullet.push_note(title, message, email=tname) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 3d8d62230ee..cd73bbba4bf 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -14,7 +14,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-pushover==0.2'] +REQUIREMENTS = ['python-pushover==0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index 038c7cd8ee9..f67eae6c611 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==4.2.1'] +REQUIREMENTS = ['sendgrid==5.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index a6257970566..30aadfc8297 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -5,20 +5,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.slack/ """ import logging -import requests -from requests.auth import HTTPDigestAuth -from requests.auth import HTTPBasicAuth +import requests +from requests.auth import HTTPBasicAuth +from requests.auth import HTTPDigestAuth import voluptuous as vol -from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, ATTR_DATA, - PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import ( - CONF_API_KEY, CONF_USERNAME, CONF_ICON) import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_ICON) -REQUIREMENTS = ['slacker==0.9.50'] +REQUIREMENTS = ['slacker==0.9.60'] _LOGGER = logging.getLogger(__name__) @@ -34,7 +33,7 @@ ATTR_FILE_PATH = 'path' ATTR_FILE_USERNAME = 'username' ATTR_FILE_PASSWORD = 'password' ATTR_FILE_AUTH = 'auth' -# Any other value or absense of 'auth' lead to basic authentication being used +# Any other value or absence of 'auth' lead to basic authentication being used ATTR_FILE_AUTH_DIGEST = 'digest' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -49,14 +48,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" import slacker + channel = config.get(CONF_CHANNEL) + api_key = config.get(CONF_API_KEY) + username = config.get(CONF_USERNAME) + icon = config.get(CONF_ICON) try: return SlackNotificationService( - config[CONF_CHANNEL], - config[CONF_API_KEY], - config.get(CONF_USERNAME, None), - config.get(CONF_ICON, None), - hass.config.is_allowed_path) + channel, api_key, username, icon, hass.config.is_allowed_path) except slacker.Error: _LOGGER.exception("Authentication failed") @@ -66,9 +65,8 @@ def get_service(hass, config, discovery_info=None): class SlackNotificationService(BaseNotificationService): """Implement the notification service for Slack.""" - def __init__(self, default_channel, - api_token, username, - icon, is_allowed_path): + def __init__( + self, default_channel, api_token, username, icon, is_allowed_path): """Initialize the service.""" from slacker import Slacker self._default_channel = default_channel @@ -101,7 +99,7 @@ class SlackNotificationService(BaseNotificationService): for target in targets: try: if file is not None: - # Load from file or url + # Load from file or URL file_as_bytes = self.load_file( url=file.get(ATTR_FILE_URL), local_path=file.get(ATTR_FILE_PATH), @@ -113,7 +111,7 @@ class SlackNotificationService(BaseNotificationService): filename = file.get(ATTR_FILE_URL) else: filename = file.get(ATTR_FILE_PATH) - # Prepare structure for slack API + # Prepare structure for Slack API data = { 'content': None, 'filetype': None, @@ -135,35 +133,33 @@ class SlackNotificationService(BaseNotificationService): except slacker.Error as err: _LOGGER.error("Could not send notification. Error: %s", err) - def load_file(self, url=None, local_path=None, - username=None, password=None, auth=None): - """Load image/document/etc from a local path or url.""" + def load_file(self, url=None, local_path=None, username=None, + password=None, auth=None): + """Load image/document/etc from a local path or URL.""" try: if url is not None: - # check whether authentication parameters are provided + # Check whether authentication parameters are provided if username is not None and password is not None: # Use digest or basic authentication if ATTR_FILE_AUTH_DIGEST == auth: auth_ = HTTPDigestAuth(username, password) else: auth_ = HTTPBasicAuth(username, password) - # load file from url with authentication + # Load file from URL with authentication req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT) else: - # load file from url without authentication + # Load file from URL without authentication req = requests.get(url, timeout=CONF_TIMEOUT) return req.content elif local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - # load file from local path on server return open(local_path, "rb") _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: - # neither url nor path provided - _LOGGER.warning("Neither url nor local path found in params!") + _LOGGER.warning("Neither URL nor local path found in params!") except OSError as error: _LOGGER.error("Can't load from url or local path: %s", error) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 204490ce36c..fdf237d7180 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -16,11 +16,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = 'octoprint' +CONF_NUMBER_OF_TOOLS = 'number_of_tools' +CONF_BED = 'bed' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, + vol.Optional(CONF_BED, default=False): cv.boolean }), }, extra=vol.ALLOW_EXTRA) @@ -29,11 +33,13 @@ def setup(hass, config): """Set up the OctoPrint component.""" base_url = 'http://{}/api/'.format(config[DOMAIN][CONF_HOST]) api_key = config[DOMAIN][CONF_API_KEY] + number_of_tools = config[DOMAIN][CONF_NUMBER_OF_TOOLS] + bed = config[DOMAIN][CONF_BED] hass.data[DOMAIN] = {"api": None} try: - octoprint_api = OctoPrintAPI(base_url, api_key) + octoprint_api = OctoPrintAPI(base_url, api_key, bed, number_of_tools) hass.data[DOMAIN]["api"] = octoprint_api octoprint_api.get('printer') octoprint_api.get('job') @@ -46,7 +52,7 @@ def setup(hass, config): class OctoPrintAPI(object): """Simple JSON wrapper for OctoPrint's API.""" - def __init__(self, api_url, key): + def __init__(self, api_url, key, bed, number_of_tools): """Initialize OctoPrint API and set headers needed later.""" self.api_url = api_url self.headers = {'content-type': CONTENT_TYPE_JSON, @@ -58,11 +64,23 @@ class OctoPrintAPI(object): self.available = False self.printer_error_logged = False self.job_error_logged = False + self.bed = bed + self.number_of_tools = number_of_tools + _LOGGER.error(str(bed) + " " + str(number_of_tools)) def get_tools(self): - """Get the dynamic list of tools that temperature is monitored on.""" - tools = self.printer_last_reading[0]['temperature'] - return tools.keys() + """Get the list of tools that temperature is monitored on.""" + tools = [] + if self.number_of_tools > 0: + for tool_number in range(0, self.number_of_tools): + tools.append("tool" + str(tool_number)) + if self.bed: + tools.append('bed') + if not self.bed and self.number_of_tools == 0: + temps = self.printer_last_reading[0].get('temperature') + if temps is not None: + tools = temps.keys() + return tools def get(self, endpoint): """Send a get request, and return the response as a dict.""" diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index f244bcdd740..0396cafd4ff 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -12,16 +12,18 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.components import recorder -from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, - CONF_INCLUDE, EVENT_STATE_CHANGED, - TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.const import ( + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, TEMP_CELSIUS, + EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN) from homeassistant import core as hacore from homeassistant.helpers import state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius +REQUIREMENTS = ['prometheus_client==0.0.19'] + _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['prometheus_client==0.0.19'] +API_ENDPOINT = '/api/prometheus' DOMAIN = 'prometheus' DEPENDENCIES = ['http'] @@ -30,8 +32,6 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: recorder.FILTER_SCHEMA, }, extra=vol.ALLOW_EXTRA) -API_ENDPOINT = '/api/prometheus' - def setup(hass, config): """Activate Prometheus component.""" @@ -45,11 +45,10 @@ def setup(hass, config): metrics = Metrics(prometheus_client, exclude, include) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) - return True -class Metrics: +class Metrics(object): """Model all of the metrics which should be exposed to Prometheus.""" def __init__(self, prometheus_client, exclude, include): @@ -81,7 +80,7 @@ class Metrics: entity_id not in self.include_entities): return - handler = '_handle_' + domain + handler = '_handle_{}'.format(domain) if hasattr(self, handler): getattr(self, handler)(state) @@ -233,8 +232,8 @@ class PrometheusView(HomeAssistantView): @asyncio.coroutine def get(self, request): """Handle request for Prometheus metrics.""" - _LOGGER.debug('Received Prometheus metrics request') + _LOGGER.debug("Received Prometheus metrics request") return web.Response( body=self.prometheus_client.generate_latest(), - content_type="text/plain") + content_type=CONTENT_TYPE_TEXT_PLAIN) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 62edb11b778..7be8bd8175e 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -39,13 +39,13 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' GROUP_NAME_ALL_SCRIPTS = 'all scripts' -_SCRIPT_ENTRY_SCHEMA = vol.Schema({ +SCRIPT_ENTRY_SCHEMA = vol.Schema({ CONF_ALIAS: cv.string, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: _SCRIPT_ENTRY_SCHEMA}) + DOMAIN: vol.Schema({cv.slug: SCRIPT_ENTRY_SCHEMA}) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) @@ -62,12 +62,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def reload(hass): - """Reload script component.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - @bind_hass def turn_on(hass, entity_id, variables=None): """Turn script on.""" @@ -88,6 +82,21 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) +@bind_hass +def reload(hass): + """Reload script component.""" + hass.services.call(DOMAIN, SERVICE_RELOAD) + + +@bind_hass +def async_reload(hass): + """Reload the scripts from config. + + Returns a coroutine object. + """ + return hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + @asyncio.coroutine def async_setup(hass, config): """Load the scripts from the configuration.""" diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 755d88bb443..8961fa1dc74 100755 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -23,12 +23,14 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time) from homeassistant.util import dt as dt_util -REQUIREMENTS = ['buienradar==0.8'] +REQUIREMENTS = ['buienradar==0.9'] _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = 'Measured' TIMEFRAME_LABEL = 'Timeframe' +SYMBOL = 'symbol' + # Schedule next call after (minutes): SCHEDULE_OK = 10 # When an error occurred, new call after (minutes): @@ -38,6 +40,10 @@ SCHEDULE_NOK = 2 # Key: ['label', unit, icon] SENSOR_TYPES = { 'stationname': ['Stationname', None, None], + 'condition': ['Condition', None, None], + 'conditioncode': ['Condition code', None, None], + 'conditiondetailed': ['Detailed condition', None, None], + 'conditionexact': ['Full condition', None, None], 'symbol': ['Symbol', None, None], 'humidity': ['Humidity', '%', 'mdi:water-percent'], 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], @@ -55,7 +61,67 @@ SENSOR_TYPES = { 'precipitation_forecast_average': ['Precipitation forecast average', 'mm/h', 'mdi:weather-pouring'], 'precipitation_forecast_total': ['Precipitation forecast total', - 'mm', 'mdi:weather-pouring'] + 'mm', 'mdi:weather-pouring'], + 'temperature_1d': ['Temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_2d': ['Temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_3d': ['Temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_4d': ['Temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_5d': ['Temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_1d': ['Minimum temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_2d': ['Minimum temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_3d': ['Minimum temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_4d': ['Minimum temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_5d': ['Minimum temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], + 'rain_1d': ['Rain 1d', 'mm', 'mdi:weather-pouring'], + 'rain_2d': ['Rain 2d', 'mm', 'mdi:weather-pouring'], + 'rain_3d': ['Rain 3d', 'mm', 'mdi:weather-pouring'], + 'rain_4d': ['Rain 4d', 'mm', 'mdi:weather-pouring'], + 'rain_5d': ['Rain 5d', 'mm', 'mdi:weather-pouring'], + 'snow_1d': ['Snow 1d', 'cm', 'mdi:snowflake'], + 'snow_2d': ['Snow 2d', 'cm', 'mdi:snowflake'], + 'snow_3d': ['Snow 3d', 'cm', 'mdi:snowflake'], + 'snow_4d': ['Snow 4d', 'cm', 'mdi:snowflake'], + 'snow_5d': ['Snow 5d', 'cm', 'mdi:snowflake'], + 'rainchance_1d': ['Rainchance 1d', '%', 'mdi:weather-pouring'], + 'rainchance_2d': ['Rainchance 2d', '%', 'mdi:weather-pouring'], + 'rainchance_3d': ['Rainchance 3d', '%', 'mdi:weather-pouring'], + 'rainchance_4d': ['Rainchance 4d', '%', 'mdi:weather-pouring'], + 'rainchance_5d': ['Rainchance 5d', '%', 'mdi:weather-pouring'], + 'sunchance_1d': ['Sunchance 1d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_2d': ['Sunchance 2d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_3d': ['Sunchance 3d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_4d': ['Sunchance 4d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_5d': ['Sunchance 5d', '%', 'mdi:weather-partlycloudy'], + 'windforce_1d': ['Wind force 1d', 'Bft', 'mdi:weather-windy'], + 'windforce_2d': ['Wind force 2d', 'Bft', 'mdi:weather-windy'], + 'windforce_3d': ['Wind force 3d', 'Bft', 'mdi:weather-windy'], + 'windforce_4d': ['Wind force 4d', 'Bft', 'mdi:weather-windy'], + 'windforce_5d': ['Wind force 5d', 'Bft', 'mdi:weather-windy'], + 'condition_1d': ['Condition 1d', None, None], + 'condition_2d': ['Condition 2d', None, None], + 'condition_3d': ['Condition 3d', None, None], + 'condition_4d': ['Condition 4d', None, None], + 'condition_5d': ['Condition 5d', None, None], + 'conditioncode_1d': ['Condition code 1d', None, None], + 'conditioncode_2d': ['Condition code 2d', None, None], + 'conditioncode_3d': ['Condition code 3d', None, None], + 'conditioncode_4d': ['Condition code 4d', None, None], + 'conditioncode_5d': ['Condition code 5d', None, None], + 'conditiondetailed_1d': ['Detailed condition 1d', None, None], + 'conditiondetailed_2d': ['Detailed condition 2d', None, None], + 'conditiondetailed_3d': ['Detailed condition 3d', None, None], + 'conditiondetailed_4d': ['Detailed condition 4d', None, None], + 'conditiondetailed_5d': ['Detailed condition 5d', None, None], + 'conditionexact_1d': ['Full condition 1d', None, None], + 'conditionexact_2d': ['Full condition 2d', None, None], + 'conditionexact_3d': ['Full condition 3d', None, None], + 'conditionexact_4d': ['Full condition 4d', None, None], + 'conditionexact_5d': ['Full condition 5d', None, None], + 'symbol_1d': ['Symbol 1d', None, None], + 'symbol_2d': ['Symbol 2d', None, None], + 'symbol_3d': ['Symbol 3d', None, None], + 'symbol_4d': ['Symbol 4d', None, None], + 'symbol_5d': ['Symbol 5d', None, None], } CONF_TIMEFRAME = 'timeframe' @@ -126,23 +192,86 @@ class BrSensor(Entity): def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.buienradar import (ATTRIBUTION, IMAGE, MEASURED, + from buienradar.buienradar import (ATTRIBUTION, CONDITION, CONDCODE, + DETAILED, EXACT, EXACTNL, FORECAST, + IMAGE, MEASURED, PRECIPITATION_FORECAST, STATIONNAME, - SYMBOL, TIMEFRAME) + TIMEFRAME) self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) - if self.type == SYMBOL: - # update weather symbol & status text - new_state = data.get(self.type) - img = data.get(IMAGE) - # pylint: disable=protected-access - if new_state != self._state or img != self._entity_picture: - self._state = new_state - self._entity_picture = img - return True + if self.type.endswith('_1d') or \ + self.type.endswith('_2d') or \ + self.type.endswith('_3d') or \ + self.type.endswith('_4d') or \ + self.type.endswith('_5d'): + + fcday = 0 + if self.type.endswith('_2d'): + fcday = 1 + if self.type.endswith('_3d'): + fcday = 2 + if self.type.endswith('_4d'): + fcday = 3 + if self.type.endswith('_5d'): + fcday = 4 + + # update all other sensors + if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION): + condition = data.get(FORECAST)[fcday].get(CONDITION) + if condition: + new_state = condition.get(CONDITION, None) + if self.type.startswith(SYMBOL): + new_state = condition.get(EXACTNL, None) + if self.type.startswith('conditioncode'): + new_state = condition.get(CONDCODE, None) + if self.type.startswith('conditiondetailed'): + new_state = condition.get(DETAILED, None) + if self.type.startswith('conditionexact'): + new_state = condition.get(EXACT, None) + + img = condition.get(IMAGE, None) + + if new_state != self._state or img != self._entity_picture: + self._state = new_state + self._entity_picture = img + return True + return False + else: + new_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + + if new_state != self._state: + self._state = new_state + return True + return False + + return False + + if self.type == SYMBOL or self.type.startswith(CONDITION): + # update weather symbol & status text + condition = data.get(CONDITION, None) + if condition: + if self.type == SYMBOL: + new_state = condition.get(EXACTNL, None) + if self.type == CONDITION: + new_state = condition.get(CONDITION, None) + if self.type == 'conditioncode': + new_state = condition.get(CONDCODE, None) + if self.type == 'conditiondetailed': + new_state = condition.get(DETAILED, None) + if self.type == 'conditionexact': + new_state = condition.get(EXACT, None) + + img = condition.get(IMAGE, None) + + # pylint: disable=protected-access + if new_state != self._state or img != self._entity_picture: + self._state = new_state + self._entity_picture = img + return True + return False if self.type.startswith(PRECIPITATION_FORECAST): @@ -187,11 +316,6 @@ class BrSensor(Entity): @property def entity_picture(self): """Weather symbol if type is symbol.""" - from buienradar.buienradar import SYMBOL - - if self.type != SYMBOL: - return None - return self._entity_picture @property @@ -360,8 +484,8 @@ class BrData(object): @property def condition(self): """Return the condition.""" - from buienradar.buienradar import SYMBOL - return self.data.get(SYMBOL) + from buienradar.buienradar import CONDITION + return self.data.get(CONDITION) @property def temperature(self): @@ -390,6 +514,15 @@ class BrData(object): except (ValueError, TypeError): return None + @property + def visibility(self): + """Return the visibility, or None.""" + from buienradar.buienradar import VISIBILITY + try: + return int(self.data.get(VISIBILITY)) + except (ValueError, TypeError): + return None + @property def wind_speed(self): """Return the windspeed, or None.""" @@ -402,9 +535,9 @@ class BrData(object): @property def wind_bearing(self): """Return the wind bearing, or None.""" - from buienradar.buienradar import WINDDIRECTION + from buienradar.buienradar import WINDAZIMUTH try: - return int(self.data.get(WINDDIRECTION)) + return int(self.data.get(WINDAZIMUTH)) except (ValueError, TypeError): return None diff --git a/homeassistant/components/sensor/cert_expiry.py b/homeassistant/components/sensor/cert_expiry.py index dfc15510d6f..1ccaf2f6925 100644 --- a/homeassistant/components/sensor/cert_expiry.py +++ b/homeassistant/components/sensor/cert_expiry.py @@ -4,16 +4,17 @@ Counter for the days till a HTTPS (TLS) certificate will expire. For more details about this sensor please refer to the documentation at https://home-assistant.io/components/sensor.cert_expiry/ """ -import datetime import logging import socket import ssl +from datetime import datetime, timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_START) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'SSL Certificate Expiry' DEFAULT_PORT = 443 -SCAN_INTERVAL = datetime.timedelta(hours=12) +SCAN_INTERVAL = timedelta(hours=12) TIMEOUT = 10.0 @@ -34,11 +35,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up certificate expiry sensor.""" - server_name = config.get(CONF_HOST) - server_port = config.get(CONF_PORT) - sensor_name = config.get(CONF_NAME) + def run_setup(event): + """Wait until Home Assistant is fully initialized before creating. - add_devices([SSLCertificate(sensor_name, server_name, server_port)], True) + Delay the setup until Home Assistant is fully initialized. + """ + server_name = config.get(CONF_HOST) + server_port = config.get(CONF_PORT) + sensor_name = config.get(CONF_NAME) + + add_devices([SSLCertificate(sensor_name, server_name, server_port)], + True) + + # To allow checking of the HA certificate we must first be running. + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) class SSLCertificate(Entity): @@ -97,6 +107,6 @@ class SSLCertificate(Entity): return ts_seconds = ssl.cert_time_to_seconds(cert['notAfter']) - timestamp = datetime.datetime.fromtimestamp(ts_seconds) - expiry = timestamp - datetime.datetime.today() + timestamp = datetime.fromtimestamp(ts_seconds) + expiry = timestamp - datetime.today() self._state = expiry.days diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 5da52272fb1..dc879fe0d3e 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -30,7 +30,7 @@ UNIT_OF_MEASUREMENT = 'W' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_CHANNEL_ID): cv.string, + vol.Optional(CONF_CHANNEL_ID): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index 5874e8ce487..6b159760b3c 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_date import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fedexdeliverymanager==1.0.3'] +REQUIREMENTS = ['fedexdeliverymanager==1.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index c0256e3a88b..605805c028d 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -17,10 +17,10 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component +from homeassistant.util.icon import icon_for_battery_level import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fitbit==0.2.3'] +REQUIREMENTS = ['fitbit==0.3.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -32,6 +32,7 @@ ATTR_CLIENT_SECRET = 'client_secret' ATTR_LAST_SAVED_AT = 'last_saved_at' CONF_MONITORED_RESOURCES = 'monitored_resources' +CONF_CLOCK_FORMAT = 'clock_format' CONF_ATTRIBUTION = 'Data provided by Fitbit.com' DEPENDENCIES = ['http'] @@ -49,40 +50,50 @@ DEFAULT_CONFIG = { } FITBIT_RESOURCES_LIST = { - 'activities/activityCalories': 'cal', - 'activities/calories': 'cal', - 'activities/caloriesBMR': 'cal', - 'activities/distance': '', - 'activities/elevation': '', - 'activities/floors': 'floors', - 'activities/heart': 'bpm', - 'activities/minutesFairlyActive': 'minutes', - 'activities/minutesLightlyActive': 'minutes', - 'activities/minutesSedentary': 'minutes', - 'activities/minutesVeryActive': 'minutes', - 'activities/steps': 'steps', - 'activities/tracker/activityCalories': 'cal', - 'activities/tracker/calories': 'cal', - 'activities/tracker/distance': '', - 'activities/tracker/elevation': '', - 'activities/tracker/floors': 'floors', - 'activities/tracker/minutesFairlyActive': 'minutes', - 'activities/tracker/minutesLightlyActive': 'minutes', - 'activities/tracker/minutesSedentary': 'minutes', - 'activities/tracker/minutesVeryActive': 'minutes', - 'activities/tracker/steps': 'steps', - 'body/bmi': 'BMI', - 'body/fat': '%', - 'devices/battery': 'level', - 'sleep/awakeningsCount': 'times awaken', - 'sleep/efficiency': '%', - 'sleep/minutesAfterWakeup': 'minutes', - 'sleep/minutesAsleep': 'minutes', - 'sleep/minutesAwake': 'minutes', - 'sleep/minutesToFallAsleep': 'minutes', - 'sleep/startTime': 'start time', - 'sleep/timeInBed': 'time in bed', - 'body/weight': '' + 'activities/activityCalories': ['Activity Calories', 'cal', 'fire'], + 'activities/calories': ['Calories', 'cal', 'fire'], + 'activities/caloriesBMR': ['Calories BMR', 'cal', 'fire'], + 'activities/distance': ['Distance', '', 'map-marker'], + 'activities/elevation': ['Elevation', '', 'walk'], + 'activities/floors': ['Floors', 'floors', 'walk'], + 'activities/heart': ['Resting Heart Rate', 'bpm', 'heart-pulse'], + 'activities/minutesFairlyActive': + ['Minutes Fairly Active', 'minutes', 'walk'], + 'activities/minutesLightlyActive': + ['Minutes Lightly Active', 'minutes', 'walk'], + 'activities/minutesSedentary': + ['Minutes Sedentary', 'minutes', 'seat-recline-normal'], + 'activities/minutesVeryActive': ['Minutes Very Active', 'minutes', 'run'], + 'activities/steps': ['Steps', 'steps', 'walk'], + 'activities/tracker/activityCalories': + ['Tracker Activity Calories', 'cal', 'fire'], + 'activities/tracker/calories': ['Tracker Calories', 'cal', 'fire'], + 'activities/tracker/distance': ['Tracker Distance', '', 'map-marker'], + 'activities/tracker/elevation': ['Tracker Elevation', '', 'walk'], + 'activities/tracker/floors': ['Tracker Floors', 'floors', 'walk'], + 'activities/tracker/minutesFairlyActive': + ['Tracker Minutes Fairly Active', 'minutes', 'walk'], + 'activities/tracker/minutesLightlyActive': + ['Tracker Minutes Lightly Active', 'minutes', 'walk'], + 'activities/tracker/minutesSedentary': + ['Tracker Minutes Sedentary', 'minutes', 'seat-recline-normal'], + 'activities/tracker/minutesVeryActive': + ['Tracker Minutes Very Active', 'minutes', 'run'], + 'activities/tracker/steps': ['Tracker Steps', 'steps', 'walk'], + 'body/bmi': ['BMI', 'BMI', 'human'], + 'body/fat': ['Body Fat', '%', 'human'], + 'body/weight': ['Weight', '', 'human'], + 'devices/battery': ['Battery', None, None], + 'sleep/awakeningsCount': + ['Awakenings Count', 'times awaken', 'sleep'], + 'sleep/efficiency': ['Sleep Efficiency', '%', 'sleep'], + 'sleep/minutesAfterWakeup': ['Minutes After Wakeup', 'minutes', 'sleep'], + 'sleep/minutesAsleep': ['Sleep Minutes Asleep', 'minutes', 'sleep'], + 'sleep/minutesAwake': ['Sleep Minutes Awake', 'minutes', 'sleep'], + 'sleep/minutesToFallAsleep': + ['Sleep Minutes to Fall Asleep', 'minutes', 'sleep'], + 'sleep/startTime': ['Sleep Start Time', None, 'clock'], + 'sleep/timeInBed': ['Sleep Time in Bed', 'minutes', 'hotel'] } FITBIT_MEASUREMENTS = { @@ -121,9 +132,18 @@ FITBIT_MEASUREMENTS = { } } +BATTERY_LEVELS = { + 'High': 100, + 'Medium': 50, + 'Low': 20, + 'Empty': 0 +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]), + vol.Optional(CONF_CLOCK_FORMAT, default='24H'): + vol.In(['12H', '24H']) }) @@ -155,7 +175,7 @@ def config_from_file(filename, config=None): def request_app_setup(hass, config, add_devices, config_path, discovery_info=None): """Assist user with configuring the Fitbit dev application.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # pylint: disable=unused-argument def fitbit_configuration_callback(callback_data): @@ -166,7 +186,8 @@ def request_app_setup(hass, config, add_devices, config_path, if config_file == DEFAULT_CONFIG: error_msg = ("You didn't correctly modify fitbit.conf", " please try again") - configurator.notify_errors(_CONFIGURING['fitbit'], error_msg) + configurator.notify_errors(_CONFIGURING['fitbit'], + error_msg) else: setup_platform(hass, config, add_devices, discovery_info) else: @@ -187,7 +208,7 @@ def request_app_setup(hass, config, add_devices, config_path, submit = "I have saved my Client ID and Client Secret into fitbit.conf." _CONFIGURING['fitbit'] = configurator.request_config( - hass, 'Fitbit', fitbit_configuration_callback, + 'Fitbit', fitbit_configuration_callback, description=description, submit_caption=submit, description_image="/static/images/config_fitbit_app.png" ) @@ -195,7 +216,7 @@ def request_app_setup(hass, config, add_devices, config_path, def request_oauth_completion(hass): """Request user complete Fitbit OAuth2 flow.""" - configurator = get_component('configurator') + configurator = hass.components.configurator if "fitbit" in _CONFIGURING: configurator.notify_errors( _CONFIGURING['fitbit'], "Failed to register, please try again.") @@ -211,7 +232,7 @@ def request_oauth_completion(hass): description = "Please authorize Fitbit by visiting {}".format(start_url) _CONFIGURING['fitbit'] = configurator.request_config( - hass, 'Fitbit', fitbit_configuration_callback, + 'Fitbit', fitbit_configuration_callback, description=description, submit_caption="I have authorized Fitbit." ) @@ -233,7 +254,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False if "fitbit" in _CONFIGURING: - get_component('configurator').request_done(_CONFIGURING.pop("fitbit")) + hass.components.configurator.request_done(_CONFIGURING.pop("fitbit")) import fitbit @@ -257,6 +278,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] registered_devs = authd_client.get_devices() + clock_format = config.get(CONF_CLOCK_FORMAT) for resource in config.get(CONF_MONITORED_RESOURCES): # monitor battery for all linked FitBit devices @@ -264,11 +286,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for dev_extra in registered_devs: dev.append(FitbitSensor( authd_client, config_path, resource, - hass.config.units.is_metric, dev_extra)) + hass.config.units.is_metric, clock_format, dev_extra)) else: dev.append(FitbitSensor( authd_client, config_path, resource, - hass.config.units.is_metric)) + hass.config.units.is_metric, clock_format)) add_devices(dev, True) else: @@ -361,34 +383,24 @@ class FitbitSensor(Entity): """Implementation of a Fitbit sensor.""" def __init__(self, client, config_path, resource_type, - is_metric, extra=None): + is_metric, clock_format, extra=None): """Initialize the Fitbit sensor.""" self.client = client self.config_path = config_path self.resource_type = resource_type + self.is_metric = is_metric + self.clock_format = clock_format self.extra = extra - pretty_resource = self.resource_type.replace('activities/', '') - pretty_resource = pretty_resource.replace('/', ' ') - pretty_resource = pretty_resource.title() - if pretty_resource == 'Body Bmi': - pretty_resource = 'BMI' - elif pretty_resource == 'Heart': - pretty_resource = 'Resting Heart Rate' - elif pretty_resource == 'Devices Battery': - if self.extra: - pretty_resource = \ - '{0} Battery'.format(self.extra.get('deviceVersion')) - else: - pretty_resource = 'Battery' - - self._name = pretty_resource - unit_type = FITBIT_RESOURCES_LIST[self.resource_type] + self._name = FITBIT_RESOURCES_LIST[self.resource_type][0] + if self.extra: + self._name = '{0} Battery'.format(self.extra.get('deviceVersion')) + unit_type = FITBIT_RESOURCES_LIST[self.resource_type][1] if unit_type == "": split_resource = self.resource_type.split('/') try: measurement_system = FITBIT_MEASUREMENTS[self.client.system] except KeyError: - if is_metric: + if self.is_metric: measurement_system = FITBIT_MEASUREMENTS['metric'] else: measurement_system = FITBIT_MEASUREMENTS['en_US'] @@ -414,9 +426,11 @@ class FitbitSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self.resource_type == 'devices/battery': - return 'mdi:battery-50' - return 'mdi:walk' + if self.resource_type == 'devices/battery' and self.extra: + battery_level = BATTERY_LEVELS[self.extra.get('battery')] + return icon_for_battery_level(battery_level=battery_level, + charging=None) + return 'mdi:{}'.format(FITBIT_RESOURCES_LIST[self.resource_type][2]) @property def device_state_attributes(self): @@ -427,7 +441,7 @@ class FitbitSensor(Entity): if self.extra: attrs['model'] = self.extra.get('deviceVersion') - attrs['type'] = self.extra.get('type') + attrs['type'] = self.extra.get('type').lower() return attrs @@ -438,7 +452,40 @@ class FitbitSensor(Entity): else: container = self.resource_type.replace("/", "-") response = self.client.time_series(self.resource_type, period='7d') - self._state = response[container][-1].get('value') + raw_state = response[container][-1].get('value') + if self.resource_type == 'activities/distance': + self._state = format(float(raw_state), '.2f') + elif self.resource_type == 'activities/tracker/distance': + self._state = format(float(raw_state), '.2f') + elif self.resource_type == 'body/bmi': + self._state = format(float(raw_state), '.1f') + elif self.resource_type == 'body/fat': + self._state = format(float(raw_state), '.1f') + elif self.resource_type == 'body/weight': + self._state = format(float(raw_state), '.1f') + elif self.resource_type == 'sleep/startTime': + if raw_state == '': + self._state = '-' + elif self.clock_format == '12H': + hours, minutes = raw_state.split(':') + hours, minutes = int(hours), int(minutes) + setting = 'AM' + if hours > 12: + setting = 'PM' + hours -= 12 + elif hours == 0: + hours = 12 + self._state = '{}:{} {}'.format(hours, minutes, setting) + else: + self._state = raw_state + else: + if self.is_metric: + self._state = raw_state + else: + try: + self._state = '{0:,}'.format(int(raw_state)) + except TypeError: + self._state = raw_state if self.resource_type == 'activities/heart': self._state = response[container][-1]. \ diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index a780d999b7e..063a4808915 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -216,7 +216,7 @@ class FritzBoxCallMonitor(object): self._sensor.set_attributes(att) elif line[1] == "CONNECT": self._sensor.set_state(VALUE_CONNECT) - att = {"with": line[4], "device": [3], "accepted": isotime} + att = {"with": line[4], "device": line[3], "accepted": isotime} att["with_name"] = self._sensor.number_to_name(att["with"]) self._sensor.set_attributes(att) elif line[1] == "DISCONNECT": diff --git a/homeassistant/components/sensor/geizhals.py b/homeassistant/components/sensor/geizhals.py index 5ba64dfa995..94f3f1884d1 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -80,6 +80,8 @@ class Geizwatch(Entity): @property def device_state_attributes(self): """Return the state attributes.""" + while len(self.data.prices) < 4: + self.data.prices.append("None") attrs = {'device_name': self.data.device_name, 'description': self.description, 'unit_of_measurement': self.data.unit_of_measurement, diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index c73e76ca752..72377e07c7c 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -6,6 +6,7 @@ https://home-assistant.io/ecosystem/ios/ """ from homeassistant.components import ios from homeassistant.helpers.entity import Entity +from homeassistant.util.icon import icon_for_battery_level DEPENDENCIES = ['ios'] @@ -83,44 +84,21 @@ class IOSSensor(Entity): device_battery = self._device[ios.ATTR_BATTERY] battery_state = device_battery[ios.ATTR_BATTERY_STATE] battery_level = device_battery[ios.ATTR_BATTERY_LEVEL] - rounded_level = round(battery_level, -1) - returning_icon_level = DEFAULT_ICON_LEVEL - if battery_state == ios.ATTR_BATTERY_STATE_FULL: - returning_icon_level = DEFAULT_ICON_LEVEL - if battery_state == ios.ATTR_BATTERY_STATE_CHARGING: - returning_icon_state = DEFAULT_ICON_STATE - else: - returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) - elif battery_state == ios.ATTR_BATTERY_STATE_CHARGING: - # Why is MDI missing 10, 50, 70? - if rounded_level in (20, 30, 40, 60, 80, 90, 100): - returning_icon_level = "{}-charging-{}".format( - DEFAULT_ICON_LEVEL, str(rounded_level)) - returning_icon_state = DEFAULT_ICON_STATE - else: - returning_icon_level = "{}-charging".format( - DEFAULT_ICON_LEVEL) - returning_icon_state = DEFAULT_ICON_STATE - elif battery_state == ios.ATTR_BATTERY_STATE_UNPLUGGED: - if rounded_level < 10: - returning_icon_level = "{}-outline".format( - DEFAULT_ICON_LEVEL) - returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) - elif battery_level > 95: - returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) - returning_icon_level = "{}-outline".format( - DEFAULT_ICON_LEVEL) - else: - returning_icon_level = "{}-{}".format(DEFAULT_ICON_LEVEL, - str(rounded_level)) - returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) + charging = True + icon_state = DEFAULT_ICON_STATE + if (battery_state == ios.ATTR_BATTERY_STATE_FULL or + battery_state == ios.ATTR_BATTERY_STATE_UNPLUGGED): + charging = False + icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: - returning_icon_level = "{}-unknown".format(DEFAULT_ICON_LEVEL) - returning_icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) + battery_level = None + charging = False + icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) if self.type == "state": - return returning_icon_state - return returning_icon_level + return icon_state + return icon_for_battery_level(battery_level=battery_level, + charging=charging) def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/sensor/london_air.py b/homeassistant/components/sensor/london_air.py new file mode 100644 index 00000000000..7a8ad4087b0 --- /dev/null +++ b/homeassistant/components/sensor/london_air.py @@ -0,0 +1,216 @@ +""" +Sensor for checking the status of London air. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.london_air/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +import requests + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_LOCATIONS = 'locations' +SCAN_INTERVAL = timedelta(minutes=30) +AUTHORITIES = [ + 'Barking and Dagenham', + 'Bexley', + 'Brent', + 'Camden', + 'City of London', + 'Croydon', + 'Ealing', + 'Enfield', + 'Greenwich', + 'Hackney', + 'Hammersmith and Fulham', + 'Haringey', + 'Harrow', + 'Havering', + 'Hillingdon', + 'Islington', + 'Kensington and Chelsea', + 'Kingston', + 'Lambeth', + 'Lewisham', + 'Merton', + 'Redbridge', + 'Richmond', + 'Southwark', + 'Sutton', + 'Tower Hamlets', + 'Wandsworth', + 'Westminster'] +URL = ('http://api.erg.kcl.ac.uk/AirQuality/Hourly/' + 'MonitoringIndex/GroupName=London/Json') + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_LOCATIONS, default=AUTHORITIES): + vol.All(cv.ensure_list, [vol.In(AUTHORITIES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tube sensor.""" + data = APIData() + data.update() + sensors = [] + for name in config.get(CONF_LOCATIONS): + sensors.append(AirSensor(name, data)) + + add_devices(sensors, True) + + +class APIData(object): + """Get the latest data for all authorities.""" + + def __init__(self): + """Initialize the AirData object.""" + self.data = None + + # Update only once in scan interval. + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from TFL.""" + response = requests.get(URL, timeout=10) + if response.status_code != 200: + _LOGGER.warning("Invalid response from API") + else: + self.data = parse_api_response(response.json()) + + +class AirSensor(Entity): + """Single authority air sensor.""" + + ICON = 'mdi:cloud-outline' + + def __init__(self, name, APIdata): + """Initialize the sensor.""" + self._name = name + self._api_data = APIdata + self._site_data = None + self._state = None + self._updated = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def site_data(self): + """Return the dict of sites data.""" + return self._site_data + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.ICON + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attrs = {} + attrs['updated'] = self._updated + attrs['sites'] = len(self._site_data) + attrs['data'] = self._site_data + return attrs + + def update(self): + """Update the sensor.""" + self._api_data.update() + self._site_data = self._api_data.data[self._name] + self._updated = self._site_data[0]['updated'] + sites_status = [] + for site in self._site_data: + if site['pollutants_status'] != 'no_species_data': + sites_status.append(site['pollutants_status']) + if sites_status: + self._state = max(set(sites_status), key=sites_status.count) + else: + self._state = STATE_UNKNOWN + + +def parse_species(species_data): + """Iterate over list of species at each site.""" + parsed_species_data = [] + quality_list = [] + for species in species_data: + if species['@AirQualityBand'] != 'No data': + species_dict = {} + species_dict['description'] = species['@SpeciesDescription'] + species_dict['code'] = species['@SpeciesCode'] + species_dict['quality'] = species['@AirQualityBand'] + species_dict['index'] = species['@AirQualityIndex'] + species_dict['summary'] = (species_dict['code'] + ' is ' + + species_dict['quality']) + parsed_species_data.append(species_dict) + quality_list.append(species_dict['quality']) + return parsed_species_data, quality_list + + +def parse_site(entry_sites_data): + """Iterate over all sites at an authority.""" + authority_data = [] + for site in entry_sites_data: + site_data = {} + species_data = [] + + site_data['updated'] = site['@BulletinDate'] + site_data['latitude'] = site['@Latitude'] + site_data['longitude'] = site['@Longitude'] + site_data['site_code'] = site['@SiteCode'] + site_data['site_name'] = site['@SiteName'].split("-")[-1].lstrip() + site_data['site_type'] = site['@SiteType'] + + if isinstance(site['Species'], dict): + species_data = [site['Species']] + else: + species_data = site['Species'] + + parsed_species_data, quality_list = parse_species(species_data) + + if not parsed_species_data: + parsed_species_data.append('no_species_data') + site_data['pollutants'] = parsed_species_data + + if quality_list: + site_data['pollutants_status'] = max(set(quality_list), + key=quality_list.count) + site_data['number_of_pollutants'] = len(quality_list) + else: + site_data['pollutants_status'] = 'no_species_data' + site_data['number_of_pollutants'] = 0 + + authority_data.append(site_data) + return authority_data + + +def parse_api_response(response): + """API can return dict or list of data so need to check.""" + data = dict.fromkeys(AUTHORITIES) + for authority in AUTHORITIES: + for entry in response['HourlyAirQualityIndex']['LocalAuthority']: + if entry['@LocalAuthorityName'] == authority: + + if isinstance(entry['Site'], dict): + entry_sites_data = [entry['Site']] + else: + entry_sites_data = entry['Site'] + + data[authority] = parse_site(entry_sites_data) + + return data diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index d46680c7b66..a8daf212e57 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -4,89 +4,18 @@ Support for MySensors sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ -import logging - from homeassistant.components import mysensors +from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_TEMP: [set_req.V_TEMP], - pres.S_HUM: [set_req.V_HUM], - pres.S_BARO: [set_req.V_PRESSURE, set_req.V_FORECAST], - pres.S_WIND: [set_req.V_WIND, set_req.V_GUST, set_req.V_DIRECTION], - pres.S_RAIN: [set_req.V_RAIN, set_req.V_RAINRATE], - pres.S_UV: [set_req.V_UV], - pres.S_WEIGHT: [set_req.V_WEIGHT, set_req.V_IMPEDANCE], - pres.S_POWER: [set_req.V_WATT, set_req.V_KWH], - pres.S_DISTANCE: [set_req.V_DISTANCE], - pres.S_LIGHT_LEVEL: [set_req.V_LIGHT_LEVEL], - pres.S_IR: [set_req.V_IR_RECEIVE], - pres.S_WATER: [set_req.V_FLOW, set_req.V_VOLUME], - pres.S_CUSTOM: [set_req.V_VAR1, - set_req.V_VAR2, - set_req.V_VAR3, - set_req.V_VAR4, - set_req.V_VAR5], - pres.S_SCENE_CONTROLLER: [set_req.V_SCENE_ON, - set_req.V_SCENE_OFF], - } - if float(gateway.protocol_version) < 1.5: - map_sv_types.update({ - pres.S_AIR_QUALITY: [set_req.V_DUST_LEVEL], - pres.S_DUST: [set_req.V_DUST_LEVEL], - }) - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_COLOR_SENSOR: [set_req.V_RGB], - pres.S_MULTIMETER: [set_req.V_VOLTAGE, - set_req.V_CURRENT, - set_req.V_IMPEDANCE], - pres.S_SOUND: [set_req.V_LEVEL], - pres.S_VIBRATION: [set_req.V_LEVEL], - pres.S_MOISTURE: [set_req.V_LEVEL], - pres.S_AIR_QUALITY: [set_req.V_LEVEL], - pres.S_DUST: [set_req.V_LEVEL], - }) - map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL) - - if float(gateway.protocol_version) >= 2.0: - map_sv_types.update({ - pres.S_INFO: [set_req.V_TEXT], - pres.S_GAS: [set_req.V_FLOW, set_req.V_VOLUME], - pres.S_GPS: [set_req.V_POSITION], - pres.S_WATER_QUALITY: [set_req.V_TEMP, set_req.V_PH, - set_req.V_ORP, set_req.V_EC] - }) - map_sv_types[pres.S_CUSTOM].append(set_req.V_CUSTOM) - map_sv_types[pres.S_POWER].extend( - [set_req.V_VAR, set_req.V_VA, set_req.V_POWER_FACTOR]) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsSensor, add_devices)) + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsSensor, add_devices=add_devices) -class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): +class MySensorsSensor(mysensors.MySensorsEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index 6cb6ef9a14d..df6ff0b0649 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -38,11 +38,12 @@ SENSOR_TYPES = { 'running', 0], 'processes_blocked': ['Processes Blocked', 'Count', 'system.processes', 'blocked', 0], - 'system_load': ['System Load', '15 min', 'system.processes', 'running', 2], + 'system_load': ['System Load', '15 min', 'system.load', 'load15', 2], 'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0], 'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0], 'ipv4_in': ['IPv4 In', 'kb/s', 'system.ipv4', 'received', 0], 'ipv4_out': ['IPv4 Out', 'kb/s', 'system.ipv4', 'sent', 0], + 'disk_free': ['Disk Free', 'GiB', 'disk_space._', 'avail', 2], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index e3a06e58370..85b388a1919 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -20,6 +20,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] DOMAIN = "octoprint" DEFAULT_NAME = 'OctoPrint' +NOTIFICATION_ID = 'octoprint_notification' +NOTIFICATION_TITLE = 'OctoPrint sensor setup error' SENSOR_TYPES = { 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], @@ -42,12 +44,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): octoprint_api = hass.data[DOMAIN]["api"] name = config.get(CONF_NAME) monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) + tools = octoprint_api.get_tools() + _LOGGER.error(str(tools)) + + if "Temperatures" in monitored_conditions: + if not tools: + hass.components.persistent_notification.create( + 'Your printer appears to be offline.
' + 'If you do not want to have your printer on
' + ' at all times, and you would like to monitor
' + 'temperatures, please add
' + 'bed and/or number_of_tools to your config
' + 'and restart.', + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) devices = [] types = ["actual", "target"] for octo_type in monitored_conditions: if octo_type == "Temperatures": - for tool in octoprint_api.get_tools(): + for tool in tools: for temp_type in types: new_sensor = OctoPrintSensor( octoprint_api, temp_type, temp_type, name, diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index e2b7584d865..928e855915a 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -from homeassistant.loader import get_component REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/' 'archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip' @@ -88,7 +87,7 @@ def setup_sabnzbd(base_url, apikey, name, hass, config, add_devices, sab_api): def request_configuration(host, name, hass, config, add_devices, sab_api): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if host in _CONFIGURING: configurator.notify_errors(_CONFIGURING[host], @@ -114,7 +113,6 @@ def request_configuration(host, name, hass, config, add_devices, sab_api): hass.async_add_job(success) _CONFIGURING[host] = configurator.request_config( - hass, DEFAULT_NAME, sabnzbd_configuration_callback, description=('Enter the API Key'), diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index 361ce551426..aeb4587f3df 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -13,7 +13,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT) + CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, + CONF_VALUE_TEMPLATE) REQUIREMENTS = ['pysnmp==4.3.9'] @@ -22,6 +23,8 @@ _LOGGER = logging.getLogger(__name__) CONF_BASEOID = 'baseoid' CONF_COMMUNITY = 'community' CONF_VERSION = 'version' +CONF_ACCEPT_ERRORS = 'accept_errors' +CONF_DEFAULT_VALUE = 'default_value' DEFAULT_COMMUNITY = 'public' DEFAULT_HOST = 'localhost' @@ -45,6 +48,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SNMP_VERSIONS), + vol.Optional(CONF_ACCEPT_ERRORS, default=False): cv.boolean, + vol.Optional(CONF_DEFAULT_VALUE): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template }) @@ -61,6 +67,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): baseoid = config.get(CONF_BASEOID) unit = config.get(CONF_UNIT_OF_MEASUREMENT) version = config.get(CONF_VERSION) + accept_errors = config.get(CONF_ACCEPT_ERRORS) + default_value = config.get(CONF_DEFAULT_VALUE) + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass errindication, _, _, _ = next( getCmd(SnmpEngine(), @@ -69,23 +81,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ContextData(), ObjectType(ObjectIdentity(baseoid)))) - if errindication: + if errindication and not accept_errors: _LOGGER.error("Please check the details in the configuration file") return False else: - data = SnmpData(host, port, community, baseoid, version) - add_devices([SnmpSensor(data, name, unit)], True) + data = SnmpData( + host, port, community, baseoid, version, accept_errors, + default_value) + add_devices([SnmpSensor(data, name, unit, value_template)], True) class SnmpSensor(Entity): """Representation of a SNMP sensor.""" - def __init__(self, data, name, unit_of_measurement): + def __init__(self, data, name, unit_of_measurement, + value_template): """Initialize the sensor.""" self.data = data self._name = name self._state = None self._unit_of_measurement = unit_of_measurement + self._value_template = value_template @property def name(self): @@ -105,19 +121,30 @@ class SnmpSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._state = self.data.value + value = self.data.value + + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN) + + self._state = value class SnmpData(object): """Get the latest data and update the states.""" - def __init__(self, host, port, community, baseoid, version): + def __init__(self, host, port, community, baseoid, version, accept_errors, + default_value): """Initialize the data object.""" self._host = host self._port = port self._community = community self._baseoid = baseoid self._version = SNMP_VERSIONS[version] + self._accept_errors = accept_errors + self._default_value = default_value self.value = None def update(self): @@ -133,11 +160,13 @@ class SnmpData(object): ObjectType(ObjectIdentity(self._baseoid))) ) - if errindication: + if errindication and not self._accept_errors: _LOGGER.error("SNMP error: %s", errindication) - elif errstatus: + elif errstatus and not self._accept_errors: _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[-1][int(errindex) - 1] or '?') + elif (errindication or errstatus) and self._accept_errors: + self.value = self._default_value else: for resrow in restable: - self.value = resrow[-1] + self.value = str(resrow[-1]) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index aa0be36b075..0febd8c95bc 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -136,7 +136,7 @@ class PublicTransportData(object): 'fields[]=connections/from/departureTimestamp/&' + 'fields[]=connections/', timeout=10) - connections = response.json()['connections'][:2] + connections = response.json()['connections'][1:3] try: self.times = [ diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index 17ce1036244..5d8ff49cd5b 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['uber_rides==0.4.1'] +REQUIREMENTS = ['uber_rides==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/uk_transport.py b/homeassistant/components/sensor/uk_transport.py index b9ce98ec257..bcac4b47279 100644 --- a/homeassistant/components/sensor/uk_transport.py +++ b/homeassistant/components/sensor/uk_transport.py @@ -180,9 +180,12 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): 'estimated': departure['best_departure_estimate'] }) - self._state = min(map( - _delta_mins, [bus['scheduled'] for bus in self._next_buses] - )) + if self._next_buses: + self._state = min( + _delta_mins(bus['scheduled']) + for bus in self._next_buses) + else: + self._state = None @property def device_state_attributes(self): @@ -242,10 +245,12 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): 'operator_name': departure['operator_name'] }) - self._state = min(map( - _delta_mins, - [train['scheduled'] for train in self._next_trains] - )) + if self._next_trains: + self._state = min( + _delta_mins(train['scheduled']) + for train in self._next_trains) + else: + self._state = None @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/usps.py b/homeassistant/components/sensor/usps.py index 1e818587a72..322c27e2f37 100644 --- a/homeassistant/components/sensor/usps.py +++ b/homeassistant/components/sensor/usps.py @@ -6,65 +6,44 @@ https://home-assistant.io/components/sensor.usps/ """ from collections import defaultdict import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION) +from homeassistant.components.usps import DATA_USPS +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DATE from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util.dt import now, parse_datetime -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['myusps==1.1.2'] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'usps' -SCAN_INTERVAL = timedelta(minutes=30) -COOKIE = 'usps_cookies.pickle' +DEPENDENCIES = ['usps'] + STATUS_DELIVERED = 'delivered' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string -}) - -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the USPS platform.""" - import myusps - try: - cookie = hass.config.path(COOKIE) - session = myusps.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - cookie_path=cookie) - except myusps.USPSError: - _LOGGER.exception('Could not connect to My USPS') - return False + if discovery_info is None: + return - add_devices([USPSPackageSensor(session, config.get(CONF_NAME)), - USPSMailSensor(session, config.get(CONF_NAME))], True) + usps = hass.data[DATA_USPS] + add_devices([USPSPackageSensor(usps), + USPSMailSensor(usps)], True) class USPSPackageSensor(Entity): """USPS Package Sensor.""" - def __init__(self, session, name): + def __init__(self, usps): """Initialize the sensor.""" - self._session = session - self._name = name + self._usps = usps + self._name = self._usps.name self._attributes = None self._state = None @property def name(self): """Return the name of the sensor.""" - return '{} packages'.format(self._name or DOMAIN) + return '{} packages'.format(self._name) @property def state(self): @@ -73,16 +52,16 @@ class USPSPackageSensor(Entity): def update(self): """Update device state.""" - import myusps + self._usps.update() status_counts = defaultdict(int) - for package in myusps.get_packages(self._session): + for package in self._usps.packages: status = slugify(package['primary_status']) if status == STATUS_DELIVERED and \ parse_datetime(package['date']).date() < now().date(): continue status_counts[status] += 1 self._attributes = { - ATTR_ATTRIBUTION: myusps.ATTRIBUTION + ATTR_ATTRIBUTION: self._usps.attribution } self._attributes.update(status_counts) self._state = sum(status_counts.values()) @@ -97,21 +76,26 @@ class USPSPackageSensor(Entity): """Icon to use in the frontend.""" return 'mdi:package-variant-closed' + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return 'packages' + class USPSMailSensor(Entity): """USPS Mail Sensor.""" - def __init__(self, session, name): + def __init__(self, usps): """Initialize the sensor.""" - self._session = session - self._name = name + self._usps = usps + self._name = self._usps.name self._attributes = None self._state = None @property def name(self): """Return the name of the sensor.""" - return '{} mail'.format(self._name or DOMAIN) + return '{} mail'.format(self._name) @property def state(self): @@ -120,18 +104,29 @@ class USPSMailSensor(Entity): def update(self): """Update device state.""" - import myusps - self._state = len(myusps.get_mail(self._session)) + self._usps.update() + if self._usps.mail is not None: + self._state = len(self._usps.mail) + else: + self._state = 0 @property def device_state_attributes(self): """Return the state attributes.""" - import myusps - return { - ATTR_ATTRIBUTION: myusps.ATTRIBUTION - } + attr = {} + attr[ATTR_ATTRIBUTION] = self._usps.attribution + try: + attr[ATTR_DATE] = self._usps.mail[0]['date'] + except IndexError: + pass + return attr @property def icon(self): """Icon to use in the frontend.""" return 'mdi:mailbox' + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return 'pieces' diff --git a/homeassistant/components/sensor/version.py b/homeassistant/components/sensor/version.py new file mode 100644 index 00000000000..c19d2743563 --- /dev/null +++ b/homeassistant/components/sensor/version.py @@ -0,0 +1,55 @@ +""" +Support for displaying the current version of Home Assistant. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.version/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import __version__, CONF_NAME +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Current Version" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Version sensor platform.""" + name = config.get(CONF_NAME) + + async_add_devices([VersionSensor(name)]) + + +class VersionSensor(Entity): + """Representation of a Home Assistant version sensor.""" + + def __init__(self, name): + """Initialize the Version sensor.""" + self._name = name + self._state = __version__ + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py new file mode 100644 index 00000000000..c9a42f3cb11 --- /dev/null +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -0,0 +1,113 @@ +""" +This component provides HA sensor support for the worldtides.info API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.worldtidesinfo/ +""" +import logging +import time +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, STATE_UNKNOWN) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'WorldTidesInfo' + +SCAN_INTERVAL = timedelta(seconds=3600) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the WorldTidesInfo sensor.""" + name = config.get(CONF_NAME) + + lat = config.get(CONF_LATITUDE, hass.config.latitude) + lon = config.get(CONF_LONGITUDE, hass.config.longitude) + key = config.get(CONF_API_KEY) + + if None in (lat, lon): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + + add_devices([WorldTidesInfoSensor(name, lat, lon, key)], True) + + +class WorldTidesInfoSensor(Entity): + """Representation of a WorldTidesInfo sensor.""" + + def __init__(self, name, lat, lon, key): + """Initialize the sensor.""" + self._name = name + self._lat = lat + self._lon = lon + self._key = key + self.data = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of this device.""" + attr = {} + if "High" in str(self.data['extremes'][0]['type']): + attr['high_tide_time_utc'] = self.data['extremes'][0]['date'] + attr['high_tide_height'] = self.data['extremes'][0]['height'] + attr['low_tide_time_utc'] = self.data['extremes'][1]['date'] + attr['low_tide_height'] = self.data['extremes'][1]['height'] + elif "Low" in str(self.data['extremes'][0]['type']): + attr['high_tide_time_utc'] = self.data['extremes'][1]['date'] + attr['high_tide_height'] = self.data['extremes'][1]['height'] + attr['low_tide_time_utc'] = self.data['extremes'][0]['date'] + attr['low_tide_height'] = self.data['extremes'][0]['height'] + return attr + + @property + def state(self): + """Return the state of the device.""" + if self.data: + if "High" in str(self.data['extremes'][0]['type']): + tidetime = time.strftime('%I:%M %p', time.localtime( + self.data['extremes'][0]['dt'])) + return "High tide at %s" % (tidetime) + elif "Low" in str(self.data['extremes'][0]['type']): + tidetime = time.strftime('%I:%M %p', time.localtime( + self.data['extremes'][1]['dt'])) + return "Low tide at %s" % (tidetime) + else: + return STATE_UNKNOWN + else: + return STATE_UNKNOWN + + def update(self): + """Get the latest data from WorldTidesInfo API.""" + start = int(time.time()) + resource = 'https://www.worldtides.info/api?extremes&length=86400' \ + '&key=%s&lat=%s&lon=%s&start=%s' % (self._key, self._lat, + self._lon, start) + + try: + self.data = requests.get(resource, timeout=10).json() + _LOGGER.debug("Data = %s", self.data) + _LOGGER.info("Tide data queried with start time set to: %s", + (start)) + except ValueError as err: + _LOGGER.error("Check WorldTidesInfo %s", err.args) + self.data = None + raise diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index e6e34f6de27..94259b8bb80 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -10,7 +10,6 @@ import os from datetime import timedelta from homeassistant.components.switch import SwitchDevice -from homeassistant.loader import get_component import homeassistant.util as util _CONFIGURING = {} @@ -51,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def request_configuration( device_id, insteonhub, model, hass, add_devices_callback): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator # We got an error if this method is called while we are configuring if device_id in _CONFIGURING: @@ -66,7 +65,7 @@ def request_configuration( add_devices_callback) _CONFIGURING[device_id] = configurator.request_config( - hass, 'Insteon Switch ' + model + ' addr: ' + device_id, + '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', @@ -79,7 +78,7 @@ def setup_switch(device_id, name, insteonhub, hass, add_devices_callback): """Set up the switch.""" if device_id in _CONFIGURING: request_id = _CONFIGURING.pop(device_id) - configurator = get_component('configurator') + configurator = hass.components.configurator configurator.request_done(request_id) _LOGGER.info("Device configuration done") diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 95f9a779327..308cce4de46 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -24,17 +24,25 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' + DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_AVAILABLE = 'ON' +DEFAULT_PAYLOAD_NOT_AVAILABLE = 'OFF' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_AVAILABILITY_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD_AVAILABLE, + default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, }) @@ -58,6 +66,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_OPTIMISTIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, )]) @@ -67,7 +77,7 @@ class MqttSwitch(SwitchDevice): def __init__(self, name, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, - value_template): + payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" self._state = False self._name = name @@ -81,6 +91,8 @@ class MqttSwitch(SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic self._template = value_template + self._payload_available = payload_available + self._payload_not_available = payload_not_available @asyncio.coroutine def async_added_to_hass(self): @@ -104,9 +116,9 @@ class MqttSwitch(SwitchDevice): @callback def availability_message_received(topic, payload, qos): """Handle new MQTT availability messages.""" - if payload == self._payload_on: + if payload == self._payload_available: self._available = True - elif payload == self._payload_off: + elif payload == self._payload_not_available: self._available = False self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 38f67ee3ee9..131ec58ae67 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -4,7 +4,6 @@ Support for MySensors switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mysensors/ """ -import logging import os import voluptuous as vol @@ -15,9 +14,6 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.config import load_yaml_config_file from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - ATTR_IR_CODE = 'V_IR_SEND' SERVICE_SEND_IR_CODE = 'mysensors_send_ir_code' @@ -29,82 +25,37 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the mysensors platform for switches.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - platform_devices = [] - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DOOR: [set_req.V_ARMED], - pres.S_MOTION: [set_req.V_ARMED], - pres.S_SMOKE: [set_req.V_ARMED], - pres.S_LIGHT: [set_req.V_LIGHT], - pres.S_LOCK: [set_req.V_LOCK_STATUS], - pres.S_IR: [set_req.V_IR_SEND], - } - device_class_map = { - pres.S_DOOR: MySensorsSwitch, - pres.S_MOTION: MySensorsSwitch, - pres.S_SMOKE: MySensorsSwitch, - pres.S_LIGHT: MySensorsSwitch, - pres.S_LOCK: MySensorsSwitch, - pres.S_IR: MySensorsIRSwitch, - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_BINARY: [set_req.V_STATUS, set_req.V_LIGHT], - pres.S_SPRINKLER: [set_req.V_STATUS], - pres.S_WATER_LEAK: [set_req.V_ARMED], - pres.S_SOUND: [set_req.V_ARMED], - pres.S_VIBRATION: [set_req.V_ARMED], - pres.S_MOISTURE: [set_req.V_ARMED], - }) - map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS) - device_class_map.update({ - pres.S_BINARY: MySensorsSwitch, - pres.S_SPRINKLER: MySensorsSwitch, - pres.S_WATER_LEAK: MySensorsSwitch, - pres.S_SOUND: MySensorsSwitch, - pres.S_VIBRATION: MySensorsSwitch, - pres.S_MOISTURE: MySensorsSwitch, - }) - if float(gateway.protocol_version) >= 2.0: - map_sv_types.update({ - pres.S_WATER_QUALITY: [set_req.V_STATUS], - }) - device_class_map.update({ - pres.S_WATER_QUALITY: MySensorsSwitch, - }) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, device_class_map, add_devices)) - platform_devices.append(devices) + device_class_map = { + 'S_DOOR': MySensorsSwitch, + 'S_MOTION': MySensorsSwitch, + 'S_SMOKE': MySensorsSwitch, + 'S_LIGHT': MySensorsSwitch, + 'S_LOCK': MySensorsSwitch, + 'S_IR': MySensorsIRSwitch, + 'S_BINARY': MySensorsSwitch, + 'S_SPRINKLER': MySensorsSwitch, + 'S_WATER_LEAK': MySensorsSwitch, + 'S_SOUND': MySensorsSwitch, + 'S_VIBRATION': MySensorsSwitch, + 'S_MOISTURE': MySensorsSwitch, + 'S_WATER_QUALITY': MySensorsSwitch, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + add_devices=add_devices) def send_ir_code_service(service): """Set IR code as device state attribute.""" entity_ids = service.data.get(ATTR_ENTITY_ID) ir_code = service.data.get(ATTR_IR_CODE) + devices = mysensors.get_mysensors_devices(hass, DOMAIN) if entity_ids: - _devices = [device for gw_devs in platform_devices - for device in gw_devs.values() + _devices = [device for device in devices.values() if isinstance(device, MySensorsIRSwitch) and device.entity_id in entity_ids] else: - _devices = [device for gw_devs in platform_devices - for device in gw_devs.values() + _devices = [device for device in devices.values() if isinstance(device, MySensorsIRSwitch)] kwargs = {ATTR_IR_CODE: ir_code} @@ -120,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=SEND_IR_CODE_SERVICE_SCHEMA) -class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): +class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Representation of the value of a MySensors Switch child node.""" @property @@ -131,9 +82,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): @property def is_on(self): """Return True if switch is on.""" - if self.value_type in self._values: - return self._values[self.value_type] == STATE_ON - return False + return self._values.get(self.value_type) == STATE_ON def turn_on(self, **kwargs): """Turn the switch on.""" @@ -159,24 +108,18 @@ class MySensorsIRSwitch(MySensorsSwitch): def __init__(self, *args): """Set up instance attributes.""" - MySensorsSwitch.__init__(self, *args) + super().__init__(*args) self._ir_code = None @property def is_on(self): """Return True if switch is on.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT in self._values: - return self._values[set_req.V_LIGHT] == STATE_ON - return False + return self._values.get(set_req.V_LIGHT) == STATE_ON def turn_on(self, **kwargs): """Turn the IR switch on.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT not in self._values: - _LOGGER.error('missing value_type: %s at node: %s, child: %s', - set_req.V_LIGHT.name, self.node_id, self.child_id) - return if ATTR_IR_CODE in kwargs: self._ir_code = kwargs[ATTR_IR_CODE] self.gateway.set_child_value( @@ -194,10 +137,6 @@ class MySensorsIRSwitch(MySensorsSwitch): def turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT not in self._values: - _LOGGER.error('missing value_type: %s at node: %s, child: %s', - set_req.V_LIGHT.name, self.node_id, self.child_id) - return self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0) if self.gateway.optimistic: @@ -207,6 +146,5 @@ class MySensorsIRSwitch(MySensorsSwitch): def update(self): """Update the controller with the latest value from a sensor.""" - MySensorsSwitch.update(self) - if self.value_type in self._values: - self._ir_code = self._values[self.value_type] + super().update() + self._ir_code = self._values.get(self.value_type) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index b56367e80be..201aee0f58c 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -4,6 +4,7 @@ Support for switching devices via Pilight to on and off. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.pilight/ """ +import asyncio import logging import voluptuous as vol @@ -12,7 +13,8 @@ import homeassistant.helpers.config_validation as cv import homeassistant.components.pilight as pilight from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE, - CONF_PROTOCOL) + CONF_PROTOCOL, STATE_ON) +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -120,6 +122,13 @@ class PilightSwitch(SwitchDevice): if any(self._code_on_receive) or any(self._code_off_receive): hass.bus.listen(pilight.EVENT, self._handle_code) + @asyncio.coroutine + def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + state = yield from async_get_last_state(self._hass, self.entity_id) + if state: + self._state = state.state == STATE_ON + @property def name(self): """Get the name of the switch.""" @@ -130,6 +139,11 @@ class PilightSwitch(SwitchDevice): """No polling needed, state set when correct code is received.""" return False + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 648fad21a8b..94ac98c1737 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -8,20 +8,21 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.switch import SwitchDevice -from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, - CONF_EMAIL, CONF_IP_ADDRESS, CONF_PASSWORD, - CONF_PLATFORM, CONF_SCAN_INTERVAL) +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS, + CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL) from homeassistant.util import Throttle _LOGGER = getLogger(__name__) -REQUIREMENTS = ['regenmaschine==0.3.2'] +REQUIREMENTS = ['regenmaschine==0.4.1'] ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' -CONF_HIDE_DISABLED_ENTITIES = 'hide_disabled_entities' CONF_ZONE_RUN_TIME = 'zone_run_time' +DEFAULT_PORT = 8080 +DEFAULT_SSL = True DEFAULT_ZONE_RUN_SECONDS = 60 * 10 MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) @@ -42,10 +43,12 @@ PLATFORM_SCHEMA = vol.Schema( vol.Email(), # pylint: disable=no-value-for-parameter vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): + cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): + cv.boolean, vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int, - vol.Optional(CONF_HIDE_DISABLED_ENTITIES, default=True): - cv.boolean + cv.positive_int }), extra=vol.ALLOW_EXTRA) @@ -64,28 +67,34 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) _LOGGER.debug('Password: %s', password) - hide_disabled_entities = config.get(CONF_HIDE_DISABLED_ENTITIES) - _LOGGER.debug('Show disabled entities: %s', hide_disabled_entities) - zone_run_time = config.get(CONF_ZONE_RUN_TIME) _LOGGER.debug('Zone run time: %s', zone_run_time) try: if ip_address: - _LOGGER.debug('Configuring local API...') - auth = rm.Authenticator.create_local(ip_address, password) + port = config.get(CONF_PORT) + _LOGGER.debug('Port: %s', port) + + ssl = config.get(CONF_SSL) + _LOGGER.debug('SSL: %s', ssl) + + _LOGGER.debug('Configuring local API') + auth = rm.Authenticator.create_local( + ip_address, password, port=port, https=ssl) elif email_address: - _LOGGER.debug('Configuring remote API...') + _LOGGER.debug('Configuring remote API') auth = rm.Authenticator.create_remote(email_address, password) - _LOGGER.debug('Instantiating RainMachine client...') + _LOGGER.debug('Querying against: %s', auth.url) + + _LOGGER.debug('Instantiating RainMachine client') client = rm.Client(auth) rainmachine_device_name = client.provision.device_name().get('name') entities = [] for program in client.programs.all().get('programs'): - if hide_disabled_entities and program.get('active') is False: + if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) @@ -94,7 +103,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): client, program, device_name=rainmachine_device_name)) for zone in client.zones.all().get('zones'): - if hide_disabled_entities and zone.get('active') is False: + if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) diff --git a/homeassistant/components/switch/xiaomi.py b/homeassistant/components/switch/xiaomi.py index 3e4ea4f6d72..767043a8bc9 100644 --- a/homeassistant/components/switch/xiaomi.py +++ b/homeassistant/components/switch/xiaomi.py @@ -6,9 +6,13 @@ from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice) _LOGGER = logging.getLogger(__name__) -ATTR_LOAD_POWER = 'Load power' # Load power in watts (W) -ATTR_POWER_CONSUMED = 'Power consumed' -ATTR_IN_USE = 'In use' +# Load power in watts (W) +ATTR_LOAD_POWER = 'load_power' + +# Total (lifetime) power consumption in watts +ATTR_POWER_CONSUMED = 'power_consumed' +ATTR_IN_USE = 'in_use' + LOAD_POWER = 'load_power' POWER_CONSUMED = 'power_consumed' IN_USE = 'inuse' diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index cd83f81afd1..31938cd15ff 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -14,7 +14,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.const import CONF_HOST, CONF_API_KEY -from homeassistant.loader import get_component from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI REQUIREMENTS = ['pytradfri==1.1'] @@ -41,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) def request_configuration(hass, config, host): """Request configuration steps from the user.""" - configurator = get_component('configurator') + configurator = hass.components.configurator hass.data.setdefault(KEY_CONFIG, {}) instance = hass.data[KEY_CONFIG].get(host) @@ -70,7 +69,7 @@ def request_configuration(hass, config, host): hass.async_add_job(success) instance = configurator.request_config( - hass, "IKEA Trådfri", configuration_callback, + "IKEA Trådfri", configuration_callback, description='Please enter the security code written at the bottom of ' 'your IKEA Trådfri Gateway.', submit_caption="Confirm", diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py new file mode 100644 index 00000000000..fdafbbc3587 --- /dev/null +++ b/homeassistant/components/usps.py @@ -0,0 +1,85 @@ +""" +Support for USPS packages and mail. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/usps/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_USERNAME, CONF_PASSWORD) +from homeassistant.helpers import (config_validation as cv, discovery) +from homeassistant.util import Throttle +from homeassistant.util.dt import now + +REQUIREMENTS = ['myusps==1.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'usps' +DATA_USPS = 'data_usps' +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) +COOKIE = 'usps_cookies.pickle' + +USPS_TYPE = ['sensor', 'camera'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DOMAIN): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Use config values to set up a function enabling status retrieval.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + name = conf.get(CONF_NAME) + + import myusps + try: + cookie = hass.config.path(COOKIE) + session = myusps.get_session(username, password, cookie_path=cookie) + except myusps.USPSError: + _LOGGER.exception('Could not connect to My USPS') + return False + + hass.data[DATA_USPS] = USPSData(session, name) + + for component in USPS_TYPE: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class USPSData(object): + """Stores the data retrieved from USPS. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, session, name): + """Initialize the data oject.""" + self.session = session + self.name = name + self.packages = [] + self.mail = [] + self.attribution = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """Fetch the latest info from USPS.""" + import myusps + self.packages = myusps.get_packages(self.session) + self.mail = myusps.get_mail(self.session, now().date()) + self.attribution = myusps.ATTRIBUTION + _LOGGER.debug("Mail, request date: %s, list: %s", + now().date(), self.mail) + _LOGGER.debug("Package list: %s", self.packages) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index bca50182a16..f37914b3b0f 100755 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/weather.buienradar/ import logging import asyncio from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA) + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) from homeassistant.const import \ CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv @@ -16,14 +16,37 @@ from homeassistant.components.sensor.buienradar import ( BrData) import voluptuous as vol -REQUIREMENTS = ['buienradar==0.8'] +REQUIREMENTS = ['buienradar==0.9'] _LOGGER = logging.getLogger(__name__) +DATA_CONDITION = 'buienradar_condition' + DEFAULT_TIMEFRAME = 60 CONF_FORECAST = 'forecast' +ATTR_FORECAST_CONDITION = 'condition' +ATTR_FORECAST_TEMP_LOW = 'templow' + + +CONDITION_CLASSES = { + 'cloudy': ['c', 'p'], + 'fog': ['d', 'n'], + 'hail': [], + 'lightning': ['g'], + 'lightning-rainy': ['s'], + 'partlycloudy': ['b', 'j', 'o', 'r'], + 'pouring': ['l', 'q'], + 'rainy': ['f', 'h', 'k', 'm'], + 'snowy': ['u', 'i', 'v', 't'], + 'snowy-rainy': ['w'], + 'sunny': ['a'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, @@ -50,8 +73,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # create weather device: _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) - async_add_devices([BrWeather(data, config.get(CONF_FORECAST, True), - config.get(CONF_NAME, None))]) + + # create condition helper + if DATA_CONDITION not in hass.data: + cond_keys = [str(chr(x)) for x in range(97, 123)] + hass.data[DATA_CONDITION] = dict.fromkeys(cond_keys) + for cond, condlst in CONDITION_CLASSES.items(): + for condi in condlst: + hass.data[DATA_CONDITION][condi] = cond + + async_add_devices([BrWeather(data, config)]) # schedule the first update in 1 minute from now: yield from data.schedule_update(1) @@ -60,10 +91,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BrWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, data, forecast, stationname=None): + def __init__(self, data, config): """Initialise the platform with a data instance and station name.""" - self._stationname = stationname - self._forecast = forecast + self._stationname = config.get(CONF_NAME, None) + self._forecast = config.get(CONF_FORECAST) self._data = data @property @@ -79,17 +110,32 @@ class BrWeather(WeatherEntity): @property def condition(self): - """Return the name of the sensor.""" - return self._data.condition + """Return the current condition.""" + from buienradar.buienradar import (CONDCODE) + if self._data and self._data.condition: + ccode = self._data.condition.get(CONDCODE) + if ccode: + conditions = self.hass.data.get(DATA_CONDITION) + if conditions: + return conditions.get(ccode) + + @property + def entity_picture(self): + """Return the entity picture to use in the frontend, if any.""" + from buienradar.buienradar import (IMAGE) + + if self._data and self._data.condition: + return self._data.condition.get(IMAGE, None) + return None @property def temperature(self): - """Return the name of the sensor.""" + """Return the current temperature.""" return self._data.temperature @property def pressure(self): - """Return the name of the sensor.""" + """Return the current pressure.""" return self._data.pressure @property @@ -97,14 +143,19 @@ class BrWeather(WeatherEntity): """Return the name of the sensor.""" return self._data.humidity + @property + def visibility(self): + """Return the current visibility.""" + return self._data.visibility + @property def wind_speed(self): - """Return the name of the sensor.""" + """Return the current windspeed.""" return self._data.wind_speed @property def wind_bearing(self): - """Return the name of the sensor.""" + """Return the current wind bearing (degrees).""" return self._data.wind_bearing @property @@ -114,6 +165,25 @@ class BrWeather(WeatherEntity): @property def forecast(self): - """Return the forecast.""" + """Return the forecast array.""" + from buienradar.buienradar import (CONDITION, CONDCODE, DATETIME, + MIN_TEMP, MAX_TEMP) + if self._forecast: - return self._data.forecast + fcdata_out = [] + cond = self.hass.data[DATA_CONDITION] + if self._data.forecast: + for data_in in self._data.forecast: + # remap keys from external library to + # keys understood by the weather component: + data_out = {} + condcode = data_in.get(CONDITION, []).get(CONDCODE) + + data_out[ATTR_FORECAST_TIME] = data_in.get(DATETIME) + data_out[ATTR_FORECAST_CONDITION] = cond[condcode] + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.get(MIN_TEMP) + data_out[ATTR_FORECAST_TEMP] = data_in.get(MAX_TEMP) + + fcdata_out.append(data_out) + + return fcdata_out diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 8d40f5dad48..23eb90daa89 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -13,7 +13,6 @@ from datetime import timedelta import voluptuous as vol import requests -from homeassistant.loader import get_component from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.helpers import discovery @@ -103,7 +102,7 @@ def _read_config_file(file_path): def _request_app_setup(hass, config): """Assist user with configuring the Wink dev application.""" hass.data[DOMAIN]['configurator'] = True - configurator = get_component('configurator') + configurator = hass.components.configurator # pylint: disable=unused-argument def wink_configuration_callback(callback_data): @@ -138,7 +137,7 @@ def _request_app_setup(hass, config): """.format(start_url) hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config( - hass, DOMAIN, wink_configuration_callback, + DOMAIN, wink_configuration_callback, description=description, submit_caption="submit", description_image="/static/images/config_wink.png", fields=[{'id': 'client_id', 'name': 'Client ID', 'type': 'string'}, @@ -151,7 +150,7 @@ def _request_app_setup(hass, config): def _request_oauth_completion(hass, config): """Request user complete Wink OAuth2 flow.""" hass.data[DOMAIN]['configurator'] = True - configurator = get_component('configurator') + configurator = hass.components.configurator if DOMAIN in hass.data[DOMAIN]['configuring']: configurator.notify_errors( hass.data[DOMAIN]['configuring'][DOMAIN], @@ -168,7 +167,7 @@ def _request_oauth_completion(hass, config): description = "Please authorize Wink by visiting {}".format(start_url) hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config( - hass, DOMAIN, wink_configuration_callback, + DOMAIN, wink_configuration_callback, description=description ) @@ -248,7 +247,7 @@ def setup(hass, config): if DOMAIN in hass.data[DOMAIN]['configuring']: _configurator = hass.data[DOMAIN]['configuring'] - get_component('configurator').request_done(_configurator.pop( + hass.components.configurator.request_done(_configurator.pop( DOMAIN)) # Using oauth diff --git a/homeassistant/components/xiaomi.py b/homeassistant/components/xiaomi.py index 377446a66c8..f79f414f0db 100644 --- a/homeassistant/components/xiaomi.py +++ b/homeassistant/components/xiaomi.py @@ -9,8 +9,7 @@ from homeassistant.const import (ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, REQUIREMENTS = ['https://github.com/Danielhiversen/PyXiaomiGateway/archive/' - 'aa9325fe6fdd62a8ef8c9ca1dce31d3292f484bb.zip#' - 'PyXiaomiGateway==0.2.0'] + '0.3.zip#PyXiaomiGateway==0.3.0'] ATTR_GW_MAC = 'gw_mac' ATTR_RINGTONE_ID = 'ringtone_id' @@ -67,8 +66,8 @@ def setup(hass, config): interface) _LOGGER.debug("Expecting %s gateways", len(gateways)) - for _ in range(discovery_retry): - _LOGGER.info('Discovering Xiaomi Gateways (Try %s)', _ + 1) + for k in range(discovery_retry): + _LOGGER.info('Discovering Xiaomi Gateways (Try %s)', k + 1) hass.data[PY_XIAOMI_GATEWAY].discover_gateways() if len(hass.data[PY_XIAOMI_GATEWAY].gateways) >= len(gateways): break @@ -153,8 +152,8 @@ class XiaomiDevice(Entity): self._name = '{}_{}'.format(name, self._sid) self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub - xiaomi_hub.callbacks[self._sid].append(self.push_data) self._device_state_attributes = {} + xiaomi_hub.callbacks[self._sid].append(self.push_data) self.parse_data(device['data']) self.parse_voltage(device['data']) @@ -165,7 +164,7 @@ class XiaomiDevice(Entity): @property def should_poll(self): - """Poll update device status.""" + """No polling needed.""" return False @property diff --git a/homeassistant/config.py b/homeassistant/config.py index a4b7bce5dc0..c90c4517397 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -108,6 +108,7 @@ tts: group: !include groups.yaml automation: !include automations.yaml +script: !include scripts.yaml """ @@ -173,11 +174,14 @@ def create_default_config(config_dir, detect_location=True): CONFIG_PATH as GROUP_CONFIG_PATH) from homeassistant.components.config.automation import ( CONFIG_PATH as AUTOMATION_CONFIG_PATH) + from homeassistant.components.config.script import ( + CONFIG_PATH as SCRIPT_CONFIG_PATH) config_path = os.path.join(config_dir, YAML_CONFIG_FILE) version_path = os.path.join(config_dir, VERSION_FILE) group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) + script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG} @@ -216,12 +220,15 @@ def create_default_config(config_dir, detect_location=True): with open(version_path, 'wt') as version_file: version_file.write(__version__) - with open(group_yaml_path, 'w'): + with open(group_yaml_path, 'wt'): pass with open(automation_yaml_path, 'wt') as fil: fil.write('[]') + with open(script_yaml_path, 'wt'): + pass + return config_path except IOError: diff --git a/homeassistant/const.py b/homeassistant/const.py index ab94bd3e420..a8fefcf26c4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 51 -PATCH_VERSION = '2' +MINOR_VERSION = 52 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) @@ -199,7 +199,10 @@ STATE_STANDBY = 'standby' STATE_ALARM_DISARMED = 'disarmed' STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' +STATE_ALARM_ARMED_NIGHT = 'armed_night' STATE_ALARM_PENDING = 'pending' +STATE_ALARM_ARMING = 'arming' +STATE_ALARM_DISARMING = 'disarming' STATE_ALARM_TRIGGERED = 'triggered' STATE_LOCKED = 'locked' STATE_UNLOCKED = 'unlocked' @@ -347,6 +350,7 @@ SERVICE_SHUFFLE_SET = 'shuffle_set' SERVICE_ALARM_DISARM = 'alarm_disarm' SERVICE_ALARM_ARM_HOME = 'alarm_arm_home' SERVICE_ALARM_ARM_AWAY = 'alarm_arm_away' +SERVICE_ALARM_ARM_NIGHT = 'alarm_arm_night' SERVICE_ALARM_TRIGGER = 'alarm_trigger' SERVICE_LOCK = 'lock' diff --git a/homeassistant/core.py b/homeassistant/core.py index 496bb018fbd..187dfcf1b83 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1079,6 +1079,8 @@ class Config(object): def is_allowed_path(self, path: str) -> bool: """Check if the path is valid for access from outside.""" + assert path is not None + parent = pathlib.Path(path).parent try: parent = parent.resolve() # pylint: disable=no-member diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6c74c49424e..aa6ca186a8e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -10,7 +10,8 @@ from jinja2 import contextfilter from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( - STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) + STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, + ATTR_UNIT_OF_MEASUREMENT) from homeassistant.core import State from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper @@ -181,8 +182,10 @@ class AllStates(object): def __iter__(self): """Return all states.""" - return iter(sorted(self._hass.states.async_all(), - key=lambda state: state.entity_id)) + return iter( + _wrap_state(state) for state in + sorted(self._hass.states.async_all(), + key=lambda state: state.entity_id)) def __call__(self, entity_id): """Return the states.""" @@ -200,7 +203,8 @@ class DomainStates(object): def __getattr__(self, name): """Return the states.""" - return self._hass.states.get('{}.{}'.format(self._domain, name)) + return _wrap_state( + self._hass.states.get('{}.{}'.format(self._domain, name))) def __iter__(self): """Return the iteration over all the states.""" @@ -210,6 +214,42 @@ class DomainStates(object): key=lambda state: state.entity_id)) +class TemplateState(State): + """Class to represent a state object in a template.""" + + # Inheritance is done so functions that check against State keep working + # pylint: disable=super-init-not-called + def __init__(self, state): + """Initialize template state.""" + self._state = state + + @property + def state_with_unit(self): + """Return the state concatenated with the unit if available.""" + state = object.__getattribute__(self, '_state') + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is None: + return state.state + return "{} {}".format(state.state, unit) + + def __getattribute__(self, name): + """Return an attribute of the state.""" + if name in TemplateState.__dict__: + return object.__getattribute__(self, name) + else: + return getattr(object.__getattribute__(self, '_state'), name) + + def __repr__(self): + """Representation of Template State.""" + rep = object.__getattribute__(self, '_state').__repr__() + return '