diff --git a/.coveragerc b/.coveragerc index 07d84523780..a2c0dde77b1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,6 +109,9 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py + homeassistant/components/homematicip_cloud.py + homeassistant/components/*/homematicip_cloud.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py @@ -309,6 +312,7 @@ omit = homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/ialarm.py + homeassistant/components/alarm_control_panel/ifttt.py homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py @@ -332,6 +336,7 @@ omit = homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/onvif.py + homeassistant/components/camera/proxy.py homeassistant/components/camera/ring.py homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py @@ -403,20 +408,20 @@ omit = homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/seven_segments.py - homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py + homeassistant/components/keyboard.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py - homeassistant/components/light/decora.py homeassistant/components/light/decora_wifi.py + homeassistant/components/light/decora.py homeassistant/components/light/flux_led.py homeassistant/components/light/greenwave.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py - homeassistant/components/light/lifx.py homeassistant/components/light/lifx_legacy.py + homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py homeassistant/components/light/osramlightify.py @@ -442,6 +447,7 @@ omit = homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/channels.py homeassistant/components/media_player/clementine.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py @@ -482,8 +488,8 @@ omit = homeassistant/components/media_player/vlc.py homeassistant/components/media_player/volumio.py homeassistant/components/media_player/xiaomi_tv.py - homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha_musiccast.py + homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/mycroft.py homeassistant/components/notify/aws_lambda.py @@ -491,8 +497,8 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend_tts.py + homeassistant/components/notify/clicksend.py homeassistant/components/notify/discord.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py @@ -517,6 +523,7 @@ omit = homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py + homeassistant/components/notify/stride.py homeassistant/components/notify/smtp.py homeassistant/components/notify/synology_chat.py homeassistant/components/notify/syslog.py @@ -554,7 +561,6 @@ omit = homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py - homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py @@ -576,6 +582,7 @@ omit = homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py + homeassistant/components/sensor/foobot.py homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py @@ -588,8 +595,8 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py + homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py @@ -632,8 +639,8 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py - homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial_pm.py + homeassistant/components/sensor/serial.py homeassistant/components/sensor/shodan.py homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py @@ -647,6 +654,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/syncthru.py homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/sytadin.py @@ -656,6 +664,7 @@ omit = homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py + homeassistant/components/sensor/trafikverket_weatherstation.py homeassistant/components/sensor/transmission.py homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py @@ -697,6 +706,7 @@ omit = homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py + homeassistant/components/switch/vesync.py homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 43e1c399671..9a8e6812cf3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,19 +12,18 @@ ## Checklist: - [ ] The code change is tested and works locally. + - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) If the code communicates with devices, web services, or third-party tools: - - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: - - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 diff --git a/.gitignore b/.gitignore index b3774b06bc8..bf49a1b61c1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ Icon *.iml # pytest +.pytest_cache .cache # GITHUB Proposed Python stuff: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/CODEOWNERS b/CODEOWNERS index fedab8f6ae4..d8ebc3cff56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/sensibo.py @andrey-git +homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/tile.py @bachya diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9c0c21d0d7..9ad922d7045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst index e31a1c98129..fb61cd94fe6 100644 --- a/docs/source/api/util.rst +++ b/docs/source/api/util.rst @@ -4,10 +4,10 @@ homeassistant.util package Submodules ---------- -homeassistant.util.async module +homeassistant.util.async_ module ------------------------------- -.. automodule:: homeassistant.util.async +.. automodule:: homeassistant.util.async_ :members: :undoc-members: :show-inheritance: diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 319d00e6de5..aa966027922 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str, if args.open_ui: # Imported here to avoid importing asyncio before monkey patch - from homeassistant.util.async import run_callback_threadsafe + from homeassistant.util.async_ import run_callback_threadsafe def open_browser(event): """Open the webinterface in a browser.""" @@ -335,7 +335,8 @@ def main() -> int: """Start Home Assistant.""" validate_python() - if os.environ.get('HASS_NO_MONKEY') != '1': + monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) + if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1': if sys.version_info[:2] >= (3, 6): monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 34eab679581..00822d93299 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any], if enable_log: async_enable_logging(hass, verbose, log_rotate_days, log_file) - if sys.version_info[:2] < (3, 5): - _LOGGER.warning( - 'Python 3.4 support has been deprecated and will be removed in ' - 'the beginning of 2018. Please upgrade Python or your operating ' - 'system. More info: https://home-assistant.io/blog/2017/10/06/' - 'deprecating-python-3.4-support/' - ) - core_config = config.get(core.DOMAIN, {}) try: diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 845eb81bbe0..f0db378ec15 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -12,13 +12,14 @@ import requests import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, + STATE_ALARM_ARMED_NIGHT) from homeassistant.components.egardia import ( EGARDIA_DEVICE, EGARDIA_SERVER, REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT ) -REQUIREMENTS = ['pythonegardia==1.0.38'] +DEPENDENCIES = ['egardia'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,8 @@ STATES = { 'DAY HOME': STATE_ALARM_ARMED_HOME, 'DISARM': STATE_ALARM_DISARMED, 'ARMHOME': STATE_ALARM_ARMED_HOME, + 'HOME': STATE_ALARM_ARMED_HOME, + 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, 'TRIGGERED': STATE_ALARM_TRIGGERED } diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py new file mode 100644 index 00000000000..5303c24876e --- /dev/null +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -0,0 +1,170 @@ +""" +Interfaces with alarm control panels that have to be controlled through IFTTT. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.ifttt/ +""" +import logging + +import voluptuous as vol + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.ifttt import ( + ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, + CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ifttt'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_STATES = [ + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME] + +DATA_IFTTT_ALARM = 'ifttt_alarm' +DEFAULT_NAME = "Home" + +CONF_EVENT_AWAY = "event_arm_away" +CONF_EVENT_HOME = "event_arm_home" +CONF_EVENT_NIGHT = "event_arm_night" +CONF_EVENT_DISARM = "event_disarm" + +DEFAULT_EVENT_AWAY = "alarm_arm_away" +DEFAULT_EVENT_HOME = "alarm_arm_home" +DEFAULT_EVENT_NIGHT = "alarm_arm_night" +DEFAULT_EVENT_DISARM = "alarm_disarm" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, + vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, + vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, + vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, +}) + +SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" + +PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_STATE): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a control panel managed through IFTTT.""" + if DATA_IFTTT_ALARM not in hass.data: + hass.data[DATA_IFTTT_ALARM] = [] + + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) + event_away = config.get(CONF_EVENT_AWAY) + event_home = config.get(CONF_EVENT_HOME) + event_night = config.get(CONF_EVENT_NIGHT) + event_disarm = config.get(CONF_EVENT_DISARM) + optimistic = config.get(CONF_OPTIMISTIC) + + alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home, + event_night, event_disarm, optimistic) + hass.data[DATA_IFTTT_ALARM].append(alarmpanel) + add_devices([alarmpanel]) + + async def push_state_update(service): + """Set the service state as device state attribute.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + state = service.data.get(ATTR_STATE) + devices = hass.data[DATA_IFTTT_ALARM] + if entity_ids: + devices = [d for d in devices if d.entity_id in entity_ids] + + for device in devices: + device.push_alarm_state(state) + device.async_schedule_update_ha_state() + + hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update, + schema=PUSH_ALARM_STATE_SERVICE_SCHEMA) + + +class IFTTTAlarmPanel(alarm.AlarmControlPanel): + """Representation of an alarm control panel controlled throught IFTTT.""" + + def __init__(self, name, code, event_away, event_home, event_night, + event_disarm, optimistic): + """Initialize the alarm control panel.""" + self._name = name + self._code = code + self._event_away = event_away + self._event_home = event_home + self._event_night = event_night + self._event_disarm = event_disarm + self._optimistic = optimistic + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def assumed_state(self): + """Notify that this platform return an assumed state.""" + return True + + @property + def code_format(self): + """Return one or more characters.""" + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) + + def set_alarm_state(self, event, state): + """Call the IFTTT trigger service to change the alarm state.""" + data = {ATTR_EVENT: event} + + self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + _LOGGER.debug("Called IFTTT component to trigger event %s", event) + if self._optimistic: + self._state = state + + def push_alarm_state(self, value): + """Push the alarm state to the given value.""" + if value in ALLOWED_STATES: + _LOGGER.debug("Pushed the alarm state to %s", value) + self._state = value + + def _check_code(self, code): + return self._code is None or self._code == code diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 72784c8178c..391de2033c7 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime: code: description: A required code to toggle the alarm control panel chime with. example: 1234 + +ifttt_push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 0d325534266..5e5155b3db8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -438,9 +438,7 @@ class _LightCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & light.SUPPORT_BRIGHTNESS: yield _AlexaBrightnessController(self.entity) - if supported & light.SUPPORT_RGB_COLOR: - yield _AlexaColorController(self.entity) - if supported & light.SUPPORT_XY_COLOR: + if supported & light.SUPPORT_COLOR: yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield _AlexaColorTemperatureController(self.entity) @@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity): @asyncio.coroutine def async_api_set_color(hass, config, request, entity): """Process a set color request.""" - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) rgb = color_util.color_hsb_to_RGB( float(request[API_PAYLOAD]['color']['hue']), float(request[API_PAYLOAD]['color']['saturation']), float(request[API_PAYLOAD]['color']['brightness']) ) - if supported & light.SUPPORT_RGB_COLOR > 0: - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_RGB_COLOR: rgb, - }, blocking=False) - else: - xyz = color_util.color_RGB_to_xy(*rgb) - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_XY_COLOR: (xyz[0], xyz[1]), - light.ATTR_BRIGHTNESS: xyz[2], - }, blocking=False) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_RGB_COLOR: rgb, + }, blocking=False) return api_message(request) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py new file mode 100644 index 00000000000..0f3edd86dcd --- /dev/null +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -0,0 +1,118 @@ +""" +Reads vehicle status from BMW connected drive portal. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'lids': ['Doors', 'opening'], + 'windows': ['Windows', 'opening'], + 'door_lock_state': ['Door lock state', 'safety'] +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the BMW sensors.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + add_devices(devices, True) + + +class BMWConnectedDriveSensor(BinarySensorDevice): + """Representation of a BMW vehicle binary sensor.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name, + device_class): + """Constructor.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name + self._device_class = device_class + self._state = None + + @property + def should_poll(self) -> bool: + """Data update is triggered from BMWConnectedDriveEntity.""" + return False + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + vehicle_state = self._vehicle.state + result = { + 'car': self._vehicle.modelName + } + + if self._attribute == 'lids': + for lid in vehicle_state.lids: + result[lid.name] = lid.state.value + elif self._attribute == 'windows': + for window in vehicle_state.windows: + result[window.name] = window.state.value + elif self._attribute == 'door_lock_state': + result['door_lock_state'] = vehicle_state.door_lock_state.value + + return result + + def update(self): + """Read new state data from the library.""" + vehicle_state = self._vehicle.state + + # device class opening: On means open, Off means closed + if self._attribute == 'lids': + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + self._state = not vehicle_state.all_lids_closed + if self._attribute == 'windows': + self._state = not vehicle_state.all_windows_closed + # device class safety: On means unsafe, Off means safe + if self._attribute == 'door_lock_state': + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = bool(vehicle_state.door_lock_state.value + in ('SELECTIVELOCKED', 'UNLOCKED')) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 28e78db90ec..ef3ec506e3a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ binary sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.deconz/ """ -import asyncio - from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) @@ -15,8 +13,8 @@ from homeassistant.core import callback DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ binary sensor.""" if discovery_info is None: return @@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_BINARY_SENSOR: entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) @@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice): """Set up sensor and add update callback to get data from websocket.""" self._sensor = sensor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe sensors events.""" self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id @@ -96,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" from pydeconz.sensor import PRESENCE - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } - if self._sensor.type in PRESENCE: + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in PRESENCE and self._sensor.dark: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py index ab88de9d3c9..76d90e78376 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/binary_sensor/egardia.py @@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.egardia import ( EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) _LOGGER = logging.getLogger(__name__) - +DEPENDENCIES = ['egardia'] EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', 'Door Contact': 'opening', 'IR': 'motion'} diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 09c4b5c8ea7..06079d6aa3b 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'leakSensor': 'moisture'} + 'wetLeakSensor': 'moisture'} @asyncio.coroutine @@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = discovery_info['address'] device = plm.devices[address] state_key = discovery_info['state_key'] + name = device.states[state_key].name + if name != 'dryLeakSensor': + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', - device.address.hex, device.states[state_key].name) + new_entity = InsteonPLMBinarySensor(device, state_key) - new_entity = InsteonPLMBinarySensor(device, state_key) - - async_add_devices([new_entity]) + async_add_devices([new_entity]) class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._insteon_device_state.value - return bool(sensorstate) + return bool(self._insteon_device_state.value) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 19fa02f63df..1e9359b6902 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -9,6 +9,17 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON +SENSORS = { + 'S_DOOR': 'door', + 'S_MOTION': 'motion', + 'S_SMOKE': 'smoke', + 'S_SPRINKLER': 'safety', + 'S_WATER_LEAK': 'safety', + 'S_SOUND': 'sound', + 'S_VIBRATION': 'vibration', + 'S_MOISTURE': 'moisture', +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for binary sensors.""" @@ -29,18 +40,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" pres = self.gateway.const.Presentation - class_map = { - pres.S_DOOR: 'opening', - pres.S_MOTION: 'motion', - pres.S_SMOKE: 'smoke', - } - if float(self.gateway.protocol_version) >= 1.5: - class_map.update({ - pres.S_SPRINKLER: 'sprinkler', - pres.S_WATER_LEAK: 'leak', - pres.S_SOUND: 'sound', - pres.S_VIBRATION: 'vibration', - pres.S_MOISTURE: 'moisture', - }) - if class_map.get(self.child_type) in DEVICE_CLASSES: - return class_map.get(self.child_type) + device_class = SENSORS.get(pres(self.child_type).name) + if device_class in DEVICE_CLASSES: + return device_class + return None diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 031e0aa42e5..9b4598f3c42 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 58599d3d3de..f5a7324d351 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.3'] +REQUIREMENTS = ['holidays==0.9.4'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 86048a56e22..9e9e2bafac5 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -BMW_COMPONENTS = ['device_tracker', 'sensor'] +BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] UPDATE_INTERVAL = 5 # in minutes diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index d70e7ff8946..6f92891c551 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -194,7 +194,9 @@ class WebDavCalendarData(object): @staticmethod def is_over(vevent): """Return if the event is over.""" - return dt.now() > WebDavCalendarData.get_end_date(vevent) + return dt.now() >= WebDavCalendarData.to_datetime( + WebDavCalendarData.get_end_date(vevent) + ) @staticmethod def get_hass_date(obj): @@ -230,4 +232,4 @@ class WebDavCalendarData(object): else: enddate = obj.dtstart.value + timedelta(days=1) - return WebDavCalendarData.to_datetime(enddate) + return enddate diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 098c7c70834..a8763e8ca9e 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -62,7 +62,14 @@ class GoogleCalendarData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - service = self.calendar_service.get() + from httplib2 import ServerNotFoundError + + try: + service = self.calendar_service.get() + except ServerNotFoundError: + _LOGGER.warning("Unable to connect to Google, using cached data") + return False + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index c5ae1dd3c11..02840c7d0ee 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -496,6 +496,10 @@ class TodoistProjectData(object): # We had no valid tasks return True + # Make sure the task collection is reset to prevent an + # infinite collection repeating the same tasks + self.all_project_tasks.clear() + # Organize the best tasks (so users can see all the tasks # they have, organized) while project_tasks: diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 3f8c4bedc75..2f5d8d28979 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -21,7 +21,7 @@ from homeassistant.components.camera import ( PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index d48f06539f4..3ae47ba5dee 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/ """ import asyncio import logging +import os import voluptuous as vol @@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera): def __init__(self, hass, config): """Initialize a ONVIF camera.""" - from onvif import ONVIFCamera, exceptions super().__init__() + import onvif + self._username = config.get(CONF_USERNAME) + self._password = config.get(CONF_PASSWORD) + self._host = config.get(CONF_HOST) + self._port = config.get(CONF_PORT) self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) + self._profile_index = config.get(CONF_PROFILE) self._input = None - camera = None + self._media_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/media.wsdl'.format(os.path.dirname( + onvif.__file__))) + + self._ptz_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/ptz.wsdl'.format(os.path.dirname( + onvif.__file__))) + + def obtain_input_uri(self): + """Set the input uri for the camera.""" + from onvif import exceptions + _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", + self._host, self._port) + try: - _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", - config.get(CONF_HOST), config.get(CONF_PORT)) - camera = ONVIFCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD) - ) - media_service = camera.create_media_service() - self._profiles = media_service.GetProfiles() - self._profile_index = config.get(CONF_PROFILE) - if self._profile_index >= len(self._profiles): + profiles = self._media_service.GetProfiles() + + if self._profile_index >= len(profiles): _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d." " Using the last profile.", self._name, self._profile_index) self._profile_index = -1 - req = media_service.create_type('GetStreamUri') + + req = self._media_service.create_type('GetStreamUri') + # pylint: disable=protected-access - req.ProfileToken = self._profiles[self._profile_index]._token - self._input = media_service.GetStreamUri(req).Uri.replace( - 'rtsp://', 'rtsp://{}:{}@'.format( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)), 1) + req.ProfileToken = profiles[self._profile_index]._token + uri_no_auth = self._media_service.GetStreamUri(req).Uri + uri_for_log = uri_no_auth.replace( + 'rtsp://', 'rtsp://:@', 1) + self._input = uri_no_auth.replace( + 'rtsp://', 'rtsp://{}:{}@'.format(self._username, + self._password), 1) _LOGGER.debug( "ONVIF Camera Using the following URL for %s: %s", - self._name, self._input) - except Exception as err: - _LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) - raise - try: - self._ptz = camera.create_ptz_service() + self._name, uri_for_log) + # we won't need the media service anymore + self._media_service = None except exceptions.ONVIFError as err: - self._ptz = None - _LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) + _LOGGER.debug("Couldn't setup camera '%s'. Error: %s", + self._name, err) + return def perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - if self._ptz: + from onvif import exceptions + if self._ptz_service: pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0 req = {"Velocity": { "PanTilt": {"_x": pan_val, "_y": tilt_val}, "Zoom": {"_x": zoom_val}}} - self._ptz.ContinuousMove(req) + try: + self._ptz_service.ContinuousMove(req) + except exceptions.ONVIFError as err: + if "Bad Request" in err.reason: + self._ptz_service = None + _LOGGER.debug("Camera '%s' doesn't support PTZ.", + self._name) + else: + _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when entity is added to hass.""" if ONVIF_DATA not in self.hass.data: self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES].append(self) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG + + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + ffmpeg = ImageFrame( self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._input, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def name(self): diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 9f261b89bb2..1984c21fadb 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -11,7 +11,7 @@ import async_timeout import voluptuous as vol -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index 5836a9c94dc..cec04b52047 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -4,7 +4,6 @@ Support for Xeoma Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.xeoma/ """ -import asyncio import logging import voluptuous as vol @@ -14,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['pyxeoma==1.3'] +REQUIREMENTS = ['pyxeoma==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Discover and setup Xeoma Cameras.""" from pyxeoma.xeoma import Xeoma, XeomaError @@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): xeoma = Xeoma(host, login, password) try: - yield from xeoma.async_test_connection() - discovered_image_names = yield from xeoma.async_get_image_names() + await xeoma.async_test_connection() + discovered_image_names = await xeoma.async_get_image_names() discovered_cameras = [ { CONF_IMAGE_NAME: image_name, @@ -103,12 +102,11 @@ class XeomaCamera(Camera): self._password = password self._last_image = None - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from pyxeoma.xeoma import XeomaError try: - image = yield from self._xeoma.async_get_camera_image( + image = await self._xeoma.async_get_camera_image( self._image, self._username, self._password) self._last_image = image except XeomaError as err: diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 6a4253ceca7..e64c2d5000e 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -14,10 +14,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} @@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -122,6 +122,7 @@ class Thermostat(ClimateDevice): self._climate_list = self.climate_list self._operation_list = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off'] + self._fan_list = ['auto', 'on'] self.update_without_throttle = False def update(self): @@ -180,24 +181,29 @@ class Thermostat(ClimateDevice): return self.thermostat['runtime']['desiredCool'] / 10.0 return None - @property - def desired_fan_mode(self): - """Return the desired fan mode of operation.""" - return self.thermostat['runtime']['desiredFanMode'] - @property def fan(self): - """Return the current fan state.""" + """Return the current fan status.""" if 'fan' in self.thermostat['equipmentStatus']: return STATE_ON return STATE_OFF + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.thermostat['runtime']['desiredFanMode'] + @property def current_hold_mode(self): """Return current hold mode.""" mode = self._current_hold_mode return None if mode == AWAY_MODE else mode + @property + def fan_list(self): + """Return the available fan modes.""" + return self._fan_list + @property def _current_hold_mode(self): events = self.thermostat['events'] @@ -206,7 +212,7 @@ class Thermostat(ClimateDevice): if event['type'] == 'hold': if event['holdClimateRef'] == 'away': if int(event['endDate'][0:4]) - \ - int(event['startDate'][0:4]) <= 1: + int(event['startDate'][0:4]) <= 1: # A temporary hold from away climate is a hold return 'away' # A permanent hold from away climate @@ -228,7 +234,7 @@ class Thermostat(ClimateDevice): def current_operation(self): """Return current operation.""" if self.operation_mode == 'auxHeatOnly' or \ - self.operation_mode == 'heatPump': + self.operation_mode == 'heatPump': return STATE_HEAT return self.operation_mode @@ -271,10 +277,11 @@ class Thermostat(ClimateDevice): operation = STATE_HEAT else: operation = status + return { "actual_humidity": self.thermostat['runtime']['actualHumidity'], "fan": self.fan, - "mode": self.mode, + "climate_mode": self.mode, "operation": operation, "climate_list": self.climate_list, "fan_min_on_time": self.fan_min_on_time @@ -342,25 +349,46 @@ class Thermostat(ClimateDevice): cool_temp_setpoint, heat_temp_setpoint, self.hold_preference()) _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " - "cool=%s, is=%s", heat_temp, isinstance( - heat_temp, (int, float)), cool_temp, + "cool=%s, is=%s", heat_temp, + isinstance(heat_temp, (int, float)), cool_temp, isinstance(cool_temp, (int, float))) self.update_without_throttle = True + def set_fan_mode(self, fan_mode): + """Set the fan mode. Valid values are "on" or "auto".""" + if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO): + error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" + _LOGGER.error(error) + return + + cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 + heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 + self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, + cool_temp, heat_temp, + self.hold_preference()) + + _LOGGER.info("Setting fan mode to: %s", fan_mode) + def set_temp_hold(self, temp): - """Set temperature hold in modes other than auto.""" - # Set arbitrary range when not in auto mode - if self.current_operation == STATE_HEAT: + """Set temperature hold in modes other than auto. + + Ecobee API: It is good practice to set the heat and cool hold + temperatures to be the same, if the thermostat is in either heat, cool, + auxHeatOnly, or off mode. If the thermostat is in auto mode, an + additional rule is required. The cool hold temperature must be greater + than the heat hold temperature by at least the amount in the + heatCoolMinDelta property. + https://www.ecobee.com/home/developer/api/examples/ex5.shtml + """ + if self.current_operation == STATE_HEAT or self.current_operation == \ + STATE_COOL: heat_temp = temp - cool_temp = temp + 20 - elif self.current_operation == STATE_COOL: - heat_temp = temp - 20 cool_temp = temp else: - # In auto mode set temperature between - heat_temp = temp - 10 - cool_temp = temp + 10 + delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 + heat_temp = temp - delta + cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) def set_temperature(self, **kwargs): @@ -369,8 +397,8 @@ class Thermostat(ClimateDevice): high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) - if self.current_operation == STATE_AUTO and (low_temp is not None or - high_temp is not None): + if self.current_operation == STATE_AUTO and \ + (low_temp is not None or high_temp is not None): self.set_auto_temp_hold(low_temp, high_temp) elif temp is not None: self.set_temp_hold(temp) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 5c0a3530006..820e715b00d 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 68b5eee35ef..e2a455aefc7 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice): return self._temperatures_list[-1] \ if self._temperatures_list else super().max_temp + @property + def unique_id(self): + """Return unique ID based on Sensibo ID.""" + return self._id + @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index adf0b8f51b6..e73d043d366 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -37,6 +37,7 @@ CONF_FILTER = 'filter' CONF_GOOGLE_ACTIONS = 'google_actions' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' +CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] @@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, + vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, }), @@ -110,7 +112,7 @@ class Cloud: def __init__(self, hass, mode, alexa, google_actions, cognito_client_id=None, user_pool_id=None, region=None, - relayer=None): + relayer=None, google_actions_sync_url=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode @@ -128,6 +130,7 @@ class Cloud: self.user_pool_id = user_pool_id self.region = region self.relayer = relayer + self.google_actions_sync_url = google_actions_sync_url else: info = SERVERS[mode] @@ -136,6 +139,7 @@ class Cloud: self.user_pool_id = info['user_pool_id'] self.region = info['region'] self.relayer = info['relayer'] + self.google_actions_sync_url = info['google_actions_sync_url'] @property def is_logged_in(self): diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 99075d3d02d..82128206d47 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -8,7 +8,9 @@ SERVERS = { 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', 'user_pool_id': 'us-east-1_87ll5WOP8', 'region': 'us-east-1', - 'relayer': 'wss://cloud.hass.io:8000/websocket' + 'relayer': 'wss://cloud.hass.io:8000/websocket', + 'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.' + 'amazonaws.com/prod/smart_home_sync'), } } diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3065de24180..a4b3b59f333 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Initialize the HTTP API.""" + hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudAccountView) @@ -38,12 +38,11 @@ _CLOUD_ERRORS = { def _handle_cloud_errors(handler): """Handle auth errors.""" - @asyncio.coroutine @wraps(handler) - def error_handler(view, request, *args, **kwargs): + async def error_handler(view, request, *args, **kwargs): """Handle exceptions that raise from the wrapped request handler.""" try: - result = yield from handler(view, request, *args, **kwargs) + result = await handler(view, request, *args, **kwargs) return result except (auth_api.CloudError, asyncio.TimeoutError) as err: @@ -57,6 +56,31 @@ def _handle_cloud_errors(handler): return error_handler +class GoogleActionsSyncView(HomeAssistantView): + """Trigger a Google Actions Smart Home Sync.""" + + url = '/api/cloud/google_actions/sync' + name = 'api:cloud:google_actions/sync' + + @_handle_cloud_errors + async def post(self, request): + """Trigger a Google Actions sync.""" + hass = request.app['hass'] + cloud = hass.data[DOMAIN] + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + await hass.async_add_job(auth_api.check_token, cloud) + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + req = await websession.post( + cloud.google_actions_sync_url, headers={ + 'authorization': cloud.id_token + }) + + return self.json({}, status_code=req.status) + + class CloudLoginView(HomeAssistantView): """Login to Home Assistant cloud.""" @@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle login request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job(auth_api.login, cloud, data['email'], - data['password']) + await hass.async_add_job(auth_api.login, cloud, data['email'], + data['password']) hass.async_add_job(cloud.iot.connect) # Allow cloud to start connecting. - yield from asyncio.sleep(0, loop=hass.loop) + await asyncio.sleep(0, loop=hass.loop) return self.json(_account_data(cloud)) @@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView): name = 'api:cloud:logout' @_handle_cloud_errors - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle logout request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from cloud.logout() + await cloud.logout() return self.json_message('ok') @@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView): url = '/api/cloud/account' name = 'api:cloud:account' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get account info.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] @@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): vol.All(str, vol.Length(min=6)), })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle registration request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.register, cloud, data['email'], data['password']) return self.json_message('ok') @@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle resending confirm email code request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.resend_email_confirm, cloud, data['email']) return self.json_message('ok') @@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle forgot password request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.forgot_password, cloud, data['email']) return self.json_message('ok') diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json new file mode 100644 index 00000000000..75b88f2f822 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ung\u00fcltige Objekt-ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt-ID" + }, + "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.", + "title": "W\u00e4hle eine Objekt-ID" + }, + "name": { + "data": { + "name": "Name" + }, + "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein", + "title": "Name des Test-Entity" + } + }, + "title": "Beispiel Konfig-Eintrag" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/en.json b/homeassistant/components/config_entry_example/.translations/en.json new file mode 100644 index 00000000000..ec24d01ebc8 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Invalid object ID" + }, + "step": { + "init": { + "data": { + "object_id": "Object ID" + }, + "description": "Please enter an object_id for the test entity.", + "title": "Pick object id" + }, + "name": { + "data": { + "name": "Name" + }, + "description": "Please enter a name for the test entity.", + "title": "Name of the entity" + } + }, + "title": "Config Entry Example" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json new file mode 100644 index 00000000000..054a6f372bc --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "name": { + "data": { + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json new file mode 100644 index 00000000000..f12e3fc52f1 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "object_id": "\uc624\ube0c\uc81d\ud2b8 ID" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694", + "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd" + }, + "name": { + "data": { + "name": "\uc774\ub984" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.", + "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984" + } + }, + "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json new file mode 100644 index 00000000000..7b52ac88cf0 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ongeldig object ID" + }, + "step": { + "init": { + "data": { + "object_id": "Object ID" + }, + "description": "Voer een object_id in voor het testen van de entiteit.", + "title": "Kies object id" + }, + "name": { + "data": { + "name": "Naam" + }, + "description": "Voer een naam in voor het testen van de entiteit.", + "title": "Naam van de entiteit" + } + }, + "title": "Voorbeeld van de config vermelding" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json new file mode 100644 index 00000000000..380c539f8af --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ugyldig objekt ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt ID" + }, + "description": "Vennligst skriv inn en object_id for testenheten.", + "title": "Velg objekt ID" + }, + "name": { + "data": { + "name": "Navn" + }, + "description": "Vennligst skriv inn et navn for testenheten.", + "title": "Navn p\u00e5 enheten" + } + }, + "title": "Konfigureringseksempel" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json new file mode 100644 index 00000000000..35cca168249 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu" + }, + "step": { + "init": { + "data": { + "object_id": "Identyfikator obiektu" + }, + "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.", + "title": "Wybierz identyfikator obiektu" + }, + "name": { + "data": { + "name": "Nazwa" + }, + "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.", + "title": "Nazwa jednostki" + } + }, + "title": "Przyk\u0142ad wpisu do konfiguracji" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json new file mode 100644 index 00000000000..1a4cdd6bbb7 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "init": { + "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.", + "title": "Alege\u021bi id-ul obiectului" + }, + "name": { + "data": { + "name": "Nume" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json new file mode 100644 index 00000000000..11d2d3f5e80 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Neveljaven ID objekta" + }, + "step": { + "init": { + "data": { + "object_id": "ID objekta" + }, + "description": "Prosimo, vnesite Id_objekta za testni subjekt.", + "title": "Izberite ID objekta" + }, + "name": { + "data": { + "name": "Ime" + }, + "description": "Vnesite ime za testni subjekt.", + "title": "Ime subjekta" + } + }, + "title": "Primer nastavitve" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json new file mode 100644 index 00000000000..e40c4d38e9f --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/vi.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" + }, + "step": { + "init": { + "data": { + "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "name": { + "data": { + "name": "T\u00ean" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" + } + }, + "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json new file mode 100644 index 00000000000..ee10e6d7b48 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID" + }, + "step": { + "init": { + "data": { + "object_id": "\u5bf9\u8c61 ID" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID", + "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID" + }, + "name": { + "data": { + "name": "\u540d\u79f0" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0", + "title": "\u8bbe\u5907\u540d\u79f0" + } + }, + "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example.py b/homeassistant/components/config_entry_example/__init__.py similarity index 90% rename from homeassistant/components/config_entry_example.py rename to homeassistant/components/config_entry_example/__init__.py index 2d5ea728ff3..3ebfdc3a183 100644 --- a/homeassistant/components/config_entry_example.py +++ b/homeassistant/components/config_entry_example/__init__.py @@ -62,13 +62,11 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler): return (yield from self.async_step_name()) errors = { - 'object_id': 'Invalid object id.' + 'object_id': 'invalid_object_id' } return self.async_show_form( - title='Pick object id', step_id='init', - description="Please enter an object_id for the test entity.", data_schema=vol.Schema({ 'object_id': str }), @@ -92,9 +90,7 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler): ) return self.async_show_form( - title='Name of the entity', step_id='name', - description="Please enter a name for the test entity.", data_schema=vol.Schema({ 'name': str }), diff --git a/homeassistant/components/config_entry_example/strings.json b/homeassistant/components/config_entry_example/strings.json new file mode 100644 index 00000000000..a7a8cd4025b --- /dev/null +++ b/homeassistant/components/config_entry_example/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Config Entry Example", + "step": { + "init": { + "title": "Pick object id", + "description": "Please enter an object_id for the test entity.", + "data": { + "object_id": "Object ID" + } + }, + "name": { + "title": "Name of the entity", + "description": "Please enter a name for the test entity.", + "data": { + "name": "Name" + } + } + }, + "error": { + "invalid_object_id": "Invalid object ID" + } + } +} diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index eaba08f0e89..2c159633a9b 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ ATTR_ENTITY_PICTURE from homeassistant.loader import bind_hass from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _KEY_INSTANCE = 'configurator' diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py new file mode 100755 index 00000000000..c1ea33a9cc7 --- /dev/null +++ b/homeassistant/components/cover/group.py @@ -0,0 +1,271 @@ +""" +This platform allows several cover to be grouped into one cover. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.group/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.cover import ( + DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION, + ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION, + SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, + SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, + SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, CONF_NAME, STATE_CLOSED) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change + +_LOGGER = logging.getLogger(__name__) + +KEY_OPEN_CLOSE = 'open_close' +KEY_STOP = 'stop' +KEY_POSITION = 'position' + +DEFAULT_NAME = 'Cover Group' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Group Cover platform.""" + async_add_devices( + [CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + + +class CoverGroup(CoverDevice): + """Representation of a CoverGroup.""" + + def __init__(self, name, entities): + """Initialize a CoverGroup entity.""" + self._name = name + self._is_closed = False + self._cover_position = 100 + self._tilt_position = None + self._supported_features = 0 + self._assumed_state = True + + self._entities = entities + self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + + @callback + def update_supported_features(self, entity_id, old_state, new_state, + update_state=True): + """Update dictionaries with supported features.""" + if not new_state: + for values in self._covers.values(): + values.discard(entity_id) + for values in self._tilts.values(): + values.discard(entity_id) + if update_state: + self.async_schedule_update_ha_state(True) + return + + features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if features & (SUPPORT_OPEN | SUPPORT_CLOSE): + self._covers[KEY_OPEN_CLOSE].add(entity_id) + else: + self._covers[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP): + self._covers[KEY_STOP].add(entity_id) + else: + self._covers[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_POSITION): + self._covers[KEY_POSITION].add(entity_id) + else: + self._covers[KEY_POSITION].discard(entity_id) + + if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT): + self._tilts[KEY_OPEN_CLOSE].add(entity_id) + else: + self._tilts[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP_TILT): + self._tilts[KEY_STOP].add(entity_id) + else: + self._tilts[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_TILT_POSITION): + self._tilts[KEY_POSITION].add(entity_id) + else: + self._tilts[KEY_POSITION].discard(entity_id) + + if update_state: + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register listeners.""" + for entity_id in self._entities: + new_state = self.hass.states.get(entity_id) + self.update_supported_features(entity_id, None, new_state, + update_state=False) + async_track_state_change(self.hass, self._entities, + self.update_supported_features) + await self.async_update() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def assumed_state(self): + """Enable buttons even if at end position.""" + return self._assumed_state + + @property + def should_poll(self): + """Disable polling for cover group.""" + return False + + @property + def supported_features(self): + """Flag supported features for the cover.""" + return self._supported_features + + @property + def is_closed(self): + """Return if all covers in group are closed.""" + return self._is_closed + + @property + def current_cover_position(self): + """Return current position for all covers.""" + return self._cover_position + + @property + def current_cover_tilt_position(self): + """Return current tilt position for all covers.""" + return self._tilt_position + + async def async_open_cover(self, **kwargs): + """Move the covers up.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, data, blocking=True) + + async def async_close_cover(self, **kwargs): + """Move the covers down.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True) + + async def async_stop_cover(self, **kwargs): + """Fire the stop action.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, data, blocking=True) + + async def async_set_cover_position(self, **kwargs): + """Set covers position.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION], + ATTR_POSITION: kwargs[ATTR_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True) + + async def async_open_cover_tilt(self, **kwargs): + """Tilt covers open.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True) + + async def async_close_cover_tilt(self, **kwargs): + """Tilt covers closed.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True) + + async def async_set_cover_tilt_position(self, **kwargs): + """Set tilt position.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION], + ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True) + + async def async_update(self): + """Update state and attributes.""" + self._assumed_state = False + + self._is_closed = True + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if not state: + continue + if state.state != STATE_CLOSED: + self._is_closed = False + break + + self._cover_position = None + if self._covers[KEY_POSITION]: + position = -1 + self._cover_position = 0 if self.is_closed else 100 + for entity_id in self._covers[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._cover_position = position + + self._tilt_position = None + if self._tilts[KEY_POSITION]: + position = -1 + self._tilt_position = 100 + for entity_id in self._tilts[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._tilt_position = position + + supported_features = 0 + supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \ + if self._covers[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP \ + if self._covers[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_POSITION \ + if self._covers[KEY_POSITION] else 0 + supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \ + if self._tilts[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP_TILT \ + if self._tilts[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_TILT_POSITION \ + if self._tilts[KEY_POSITION] else 0 + self._supported_features = supported_features + + if not self._assumed_state: + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if state and state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True + break diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index f4728a12a3b..4e197365a70 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice): None is unknown, 0 is closed, 100 is fully open. """ - return self._position + if self._position_template or self._position_script: + return self._position + return None @property def current_cover_tilt_position(self): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 18197b84b61..26d9fb401e4 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,6 @@ Support for deCONZ devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/deconz/ """ - -import asyncio import logging import voluptuous as vol @@ -19,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==30'] +REQUIREMENTS = ['pydeconz==32'] _LOGGER = logging.getLogger(__name__) @@ -57,30 +55,28 @@ Unlock your deCONZ gateway to register with Home Assistant. """ -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up services and configuration for deCONZ component.""" result = False - config_file = yield from hass.async_add_job( + config_file = await hass.async_add_job( load_json, hass.config.path(CONFIG_FILE)) - @asyncio.coroutine - def async_deconz_discovered(service, discovery_info): + async def async_deconz_discovered(service, discovery_info): """Call when deCONZ gateway has been found.""" deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - yield from async_request_configuration(hass, config, deconz_config) + await async_request_configuration(hass, config, deconz_config) if config_file: - result = yield from async_setup_deconz(hass, config, config_file) + result = await async_setup_deconz(hass, config, config_file) if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]: deconz_config = config[DOMAIN] if CONF_API_KEY in deconz_config: - result = yield from async_setup_deconz(hass, config, deconz_config) + result = await async_setup_deconz(hass, config, deconz_config) else: - yield from async_request_configuration(hass, config, deconz_config) + await async_request_configuration(hass, config, deconz_config) return True if not result: @@ -89,8 +85,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def async_setup_deconz(hass, config, deconz_config): +async def async_setup_deconz(hass, config, deconz_config): """Set up a deCONZ session. Load config, group, light and sensor data for server information. @@ -100,7 +95,7 @@ def async_setup_deconz(hass, config, deconz_config): from pydeconz import DeconzSession websession = async_get_clientsession(hass) deconz = DeconzSession(hass.loop, websession, **deconz_config) - result = yield from deconz.async_load_parameters() + result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") return False @@ -113,8 +108,7 @@ def async_setup_deconz(hass, config, deconz_config): hass, component, DOMAIN, {}, config)) deconz.start() - @asyncio.coroutine - def async_configure(call): + async def async_configure(call): """Set attribute of device in deCONZ. Field is a string representing a specific device in deCONZ @@ -140,7 +134,7 @@ def async_setup_deconz(hass, config, deconz_config): if field is None: _LOGGER.error('Could not find the entity %s', entity_id) return - yield from deconz.async_put_state(field, data) + await deconz.async_put_state(field, data) hass.services.async_register( DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) @@ -159,21 +153,19 @@ def async_setup_deconz(hass, config, deconz_config): return True -@asyncio.coroutine -def async_request_configuration(hass, config, deconz_config): +async def async_request_configuration(hass, config, deconz_config): """Request configuration steps from the user.""" configurator = hass.components.configurator - @asyncio.coroutine - def async_configuration_callback(data): + async def async_configuration_callback(data): """Set up actions to do when our configuration callback is called.""" from pydeconz.utils import async_get_api_key - api_key = yield from async_get_api_key(hass.loop, **deconz_config) + api_key = await async_get_api_key(hass.loop, **deconz_config) if api_key: deconz_config[CONF_API_KEY] = api_key - result = yield from async_setup_deconz(hass, config, deconz_config) + result = await async_setup_deconz(hass, config, deconz_config) if result: - yield from hass.async_add_job( + await hass.async_add_job( save_json, hass.config.path(CONFIG_FILE), deconz_config) configurator.async_request_done(request_id) return diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 9fea2bc104d..682496335a0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv from homeassistant.loader import get_component import homeassistant.util as util -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 1d9161c0d45..154fc3d2a63 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner): self.host, self.username, self.password, - port=int(self.port) + port=int(self.port), + encoding='utf-8' ) try: diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 8663930c4e6..d8a52aaaeb4 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner): # Filter clients to provided SSID list if self._ssid_filter: clients = [client for client in clients - if client['essid'] in self._ssid_filter] + if 'essid' in client and + client['essid'] in self._ssid_filter] self._clients = { client['mac']: client diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index d1045143bb2..eb53782d698 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. Knows which components handle certain types, will make sure they are loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ -import asyncio import json from datetime import timedelta import logging @@ -21,7 +20,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.2.4'] +REQUIREMENTS = ['netdisco==1.3.0'] DOMAIN = 'discovery' @@ -39,6 +38,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -54,6 +54,7 @@ SERVICE_HANDLERS = { SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), @@ -84,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Start a discovery service.""" from netdisco.discovery import NetworkDiscovery @@ -99,8 +99,7 @@ def async_setup(hass, config): # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] - @asyncio.coroutine - def new_service_found(service, info): + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in ignored_platforms: logger.info("Ignoring service: %s %s", service, info) @@ -124,15 +123,14 @@ def async_setup(hass, config): component, platform = comp_plat if platform is None: - yield from async_discover(hass, service, info, component, config) + await async_discover(hass, service, info, component, config) else: - yield from async_load_platform( + await async_load_platform( hass, component, platform, info, config) - @asyncio.coroutine - def scan_devices(now): + async def scan_devices(now): """Scan for devices.""" - results = yield from hass.async_add_job(_discover, netdisco) + results = await hass.async_add_job(_discover, netdisco) for result in results: hass.async_add_job(new_service_found(*result)) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index be7adc034a0..34758023f60 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['DoorBirdPy==0.1.2'] +REQUIREMENTS = ['DoorBirdPy==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index b7354b4f0a7..0d57740a83d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite' CONF_DOWNLOAD_DIR = 'download_dir' DOMAIN = 'downloader' +DOWNLOAD_FAILED_EVENT = 'download_failed' +DOWNLOAD_COMPLETED_EVENT = 'download_completed' SERVICE_DOWNLOAD_FILE = 'download_file' @@ -133,9 +135,19 @@ def setup(hass, config): fil.write(chunk) _LOGGER.debug("Downloading of %s done", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { + 'url': url, + 'filename': filename + }) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { + 'url': url, + 'filename': filename + }) # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 132e230c137..d1503dc74dc 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle from homeassistant.util.json import save_json -REQUIREMENTS = ['python-ecobee-api==0.0.15'] +REQUIREMENTS = ['python-ecobee-api==0.0.17'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia.py index 2cfc44a407b..f350ea56bb4 100644 --- a/homeassistant/components/egardia.py +++ b/homeassistant/components/egardia.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pythonegardia==1.0.38'] +REQUIREMENTS = ['pythonegardia==1.0.39'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index b9bc54b5c79..4df85711cfd 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py new file mode 100644 index 00000000000..3288a788e1f --- /dev/null +++ b/homeassistant/components/fan/zha.py @@ -0,0 +1,114 @@ +""" +Fans on Zigbee Home Automation networks. + +For more details on this platform, please refer to the documentation +at https://home-assistant.io/components/fan.zha/ +""" +import asyncio +import logging +from homeassistant.components import zha +from homeassistant.components.fan import ( + DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + SUPPORT_SET_SPEED) +from homeassistant.const import STATE_UNKNOWN + +DEPENDENCIES = ['zha'] + +_LOGGER = logging.getLogger(__name__) + +# Additional speeds in zigbee's ZCL +# Spec is unclear as to what this value means. On King Of Fans HBUniversal +# receiver, this means Very High. +SPEED_ON = 'on' +# The fan speed is self-regulated +SPEED_AUTO = 'auto' +# When the heated/cooled space is occupied, the fan is always on +SPEED_SMART = 'smart' + +SPEED_LIST = [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + SPEED_ON, + SPEED_AUTO, + SPEED_SMART +] + +VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Zigbee Home Automation fans.""" + discovery_info = zha.get_discovery_info(hass, discovery_info) + if discovery_info is None: + return + + async_add_devices([ZhaFan(**discovery_info)], update_before_add=True) + + +class ZhaFan(zha.Entity, FanEntity): + """Representation of a ZHA fan.""" + + _domain = DOMAIN + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._state + + @property + def is_on(self) -> bool: + """Return true if entity is on.""" + if self._state == STATE_UNKNOWN: + return False + return self._state != SPEED_OFF + + @asyncio.coroutine + def async_turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the entity on.""" + if speed is None: + speed = SPEED_MEDIUM + + yield from self.async_set_speed(speed) + + @asyncio.coroutine + def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + yield from self.async_set_speed(SPEED_OFF) + + @asyncio.coroutine + def async_set_speed(self: FanEntity, speed: str) -> None: + """Set the speed of the fan.""" + yield from self._endpoint.fan.write_attributes({ + 'fan_mode': SPEED_TO_VALUE[speed]}) + + self._state = speed + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) + new_value = result.get('fan_mode', None) + self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1f5a7576302..b2f50148bd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180310.0'] +REQUIREMENTS = ['home-assistant-frontend==20180330.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index c78d70e21e6..2f60f226042 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait): if domain != light.DOMAIN: return False - return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR) + return features & light.SUPPORT_COLOR def sync_attributes(self): """Return color spectrum attributes for a sync request.""" @@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait): """Return color spectrum query attributes.""" response = {} - # No need to handle XY color because light component will always - # convert XY to RGB if possible (which is when brightness is available) - color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR) - if color_rgb is not None: + color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) + if color_hs is not None: response['color'] = { 'spectrumRGB': int(color_util.color_rgb_to_hex( - color_rgb[0], color_rgb[1], color_rgb[2]), 16), + *color_util.color_hs_to_RGB(*color_hs)), 16), } return response @@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait): """Execute a color spectrum command.""" # Convert integer to hex format and left pad with 0's till length 6 hex_value = "{0:06x}".format(params['color']['spectrumRGB']) - color = color_util.rgb_hex_to_rgb_list(hex_value) + color = color_util.color_RGB_to_hs( + *color_util.rgb_hex_to_rgb_list(hex_value)) await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_RGB_COLOR: color + light.ATTR_HS_COLOR: color }, blocking=True) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 3ece434f3c1..67ad8066aff 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe DOMAIN = 'group' diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 540659273b3..87251a2745c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -156,7 +156,7 @@ def async_setup(hass, config): if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:access-point-network') + 'hassio', 'Hass.io', 'mdi:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index dd14bbf6811..8ab91b08a3d 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() - exclude = config[DOMAIN].get(CONF_EXCLUDE) + conf = config.get(DOMAIN, {}) + exclude = conf.get(CONF_EXCLUDE) if exclude: filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config[DOMAIN].get(CONF_INCLUDE) + include = conf.get(CONF_INCLUDE) if include: filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) - use_include_order = config[DOMAIN].get(CONF_ORDER) + use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) yield from hass.components.frontend.async_register_built_in_panel( @@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView): result = yield from hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) - result = result.values() + result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start _LOGGER.debug( @@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView): # by any entities explicitly included in the configuration. if self.use_include_order: - result = list(result) sorted_result = [] for order_entity in self.filters.included_entities: for state_list in result: diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ad70740536e..8ef8445aa70 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,154 +3,202 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ -import asyncio import logging -import re +from zlib import adler32 import voluptuous as vol -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, - TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.cover import SUPPORT_SET_POSITION +from homeassistant.const import ( + ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry +from .const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, + DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START) +from .util import ( + validate_entity_config, show_setup_message) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$" - -DOMAIN = 'homekit' REQUIREMENTS = ['HAP-python==1.1.7'] -BRIDGE_NAME = 'Home Assistant' -CONF_PIN_CODE = 'pincode' - -HOMEKIT_FILE = '.homekit.state' - - -def valid_pin(value): - """Validate pin code value.""" - match = re.match(_RE_VALID_PINCODE, str(value).strip()) - if not match: - raise vol.Invalid("Pin must be in the format: '123-45-678'") - return match.group(0) - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ - vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), - vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, }) }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Setup the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") + _LOGGER.debug('Begin setup HomeKit') conf = config[DOMAIN] - port = conf.get(CONF_PORT) - pin = str.encode(conf.get(CONF_PIN_CODE)) + port = conf[CONF_PORT] + auto_start = conf[CONF_AUTO_START] + entity_filter = conf[CONF_FILTER] + entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port) - homekit.setup_bridge(pin) + homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit.setup() + + if auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) + return True + + def handle_homekit_service_start(service): + """Handle start HomeKit service call.""" + if homekit.started: + _LOGGER.warning('HomeKit is already running') + return + homekit.start() + + hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START, + handle_homekit_service_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, homekit.start_driver) return True -def import_types(): - """Import all types from files in the HomeKit directory.""" - _LOGGER.debug("Import type files.") - # pylint: disable=unused-variable - from . import ( # noqa F401 - covers, security_systems, sensors, switches, thermostats) - - -def get_accessory(hass, state): +def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" + if not aid: + _LOGGER.warning('The entitiy "%s" is not supported, since it ' + 'generates an invalid aid, please change it.', + state.entity_id) + return None + if state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: - _LOGGER.debug("Add \"%s\" as \"%s\"", + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, - state.name) + state.name, aid=aid) + elif unit == '%': + _LOGGER.debug('Add "%s" as %s"', + state.entity_id, 'HumiditySensor') + return TYPES['HumiditySensor'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position - if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: - _LOGGER.debug("Add \"%s\" as \"%s\"", - state.entity_id, 'Window') - return TYPES['Window'](hass, state.entity_id, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_SET_POSITION: + _LOGGER.debug('Add "%s" as "%s"', + state.entity_id, 'WindowCovering') + return TYPES['WindowCovering'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') - return TYPES['SecuritySystem'](hass, state.entity_id, state.name) + return TYPES['SecuritySystem'](hass, state.entity_id, state.name, + alarm_code=config.get(ATTR_CODE), + aid=aid) elif state.domain == 'climate': - support_auto = False - features = state.attributes.get(ATTR_SUPPORTED_FEATURES) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE_HIGH # Check if climate device supports auto mode - if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \ - and (features & SUPPORT_TARGET_TEMPERATURE_LOW): - support_auto = True - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat') + support_auto = bool(features & support_temp_range) + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') return TYPES['Thermostat'](hass, state.entity_id, - state.name, support_auto) + state.name, support_auto, aid=aid) + + elif state.domain == 'light': + return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch') - return TYPES['Switch'](hass, state.entity_id, state.name) + or state.domain == 'input_boolean' or state.domain == 'script': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') + return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) return None +def generate_aid(entity_id): + """Generate accessory aid with zlib adler32.""" + aid = adler32(entity_id.encode('utf-8')) + if aid == 0 or aid == 1: + return None + return aid + + class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port): + def __init__(self, hass, port, entity_filter, entity_config): """Initialize a HomeKit object.""" self._hass = hass self._port = port + self._filter = entity_filter + self._config = entity_config + self.started = False + self.bridge = None self.driver = None - def setup_bridge(self, pin): - """Setup the bridge component to track all accessories.""" - from .accessories import HomeBridge - self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin) + def setup(self): + """Setup bridge and accessory driver.""" + from .accessories import HomeBridge, HomeDriver - def start_driver(self, event): - """Start the accessory driver.""" - from pyhap.accessory_driver import AccessoryDriver - self._hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, self.stop_driver) + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.stop) - import_types() - _LOGGER.debug("Start adding accessories.") - for state in self._hass.states.all(): - acc = get_accessory(self._hass, state) - if acc is not None: - self.bridge.add_accessory(acc) - - ip_address = get_local_ip() path = self._hass.config.path(HOMEKIT_FILE) - self.driver = AccessoryDriver(self.bridge, self._port, - ip_address, path) - _LOGGER.debug("Driver started") + self.bridge = HomeBridge(self._hass) + self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + + def add_bridge_accessory(self, state): + """Try adding accessory to bridge if configured beforehand.""" + if not state or not self._filter(state.entity_id): + return + aid = generate_aid(state.entity_id) + conf = self._config.pop(state.entity_id, {}) + acc = get_accessory(self._hass, state, aid, conf) + if acc is not None: + self.bridge.add_accessory(acc) + + def start(self, *args): + """Start the accessory driver.""" + if self.started: + return + self.started = True + + # pylint: disable=unused-variable + from . import ( # noqa F401 + type_covers, type_lights, type_security_systems, type_sensors, + type_switches, type_thermostats) + + for state in self._hass.states.all(): + self.add_bridge_accessory(state) + self.bridge.set_broker(self.driver) + + if not self.bridge.paired: + show_setup_message(self.bridge, self._hass) + + _LOGGER.debug('Driver start') self.driver.start() - def stop_driver(self, event): + def stop(self, *args): """Stop the accessory driver.""" - _LOGGER.debug("Driver stop") - if self.driver is not None: + if not self.started: + return + + _LOGGER.debug('Driver stop') + if self.driver and self.driver.run_sentinel: self.driver.stop() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1cd94070289..4c4409e6dfc 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -2,15 +2,33 @@ import logging from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory_driver import AccessoryDriver + +from homeassistant.helpers.event import async_track_state_change from .const import ( - SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, + MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) +from .util import ( + show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) +def add_preload_service(acc, service, chars=None): + """Define and return a service to be available for the accessory.""" + from pyhap.loader import get_serv_loader, get_char_loader + service = get_serv_loader().get(service) + if chars: + chars = chars if isinstance(chars, list) else [chars] + for char_name in chars: + char = get_char_loader().get(char_name) + service.add_characteristic(char) + acc.add_service(service) + return service + + def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" @@ -21,50 +39,70 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) -def add_preload_service(acc, service, chars=None, opt_chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - opt_char = get_char_loader().get(opt_char_name) - service.add_opt_characteristic(opt_char) - acc.add_service(service) - return service +def override_properties(char, properties=None, valid_values=None): + """Override characteristic property values and valid values.""" + if properties: + char.properties.update(properties) - -def override_properties(char, new_properties): - """Override characteristic property values.""" - char.properties.update(new_properties) + if valid_values: + char.properties['ValidValues'].update(valid_values) class HomeAccessory(Accessory): - """Class to extend the Accessory class.""" + """Adapter class for Accessory.""" - def __init__(self, display_name, model, category='OTHER', **kwargs): + # pylint: disable=no-member + + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, + category='OTHER', **kwargs): """Initialize a Accessory object.""" - super().__init__(display_name, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) self.category = getattr(Category, category, Category.OTHER) def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) + def run(self): + """Method called by accessory after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_state(new_state=state) + async_track_state_change( + self._hass, self._entity_id, self.update_state) + class HomeBridge(Bridge): - """Class to extend the Bridge class.""" + """Adapter class for Bridge.""" - def __init__(self, display_name, model, pincode, **kwargs): + def __init__(self, hass, name=BRIDGE_NAME, + model=BRIDGE_MODEL, **kwargs): """Initialize a Bridge object.""" - super().__init__(display_name, pincode=pincode, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) + self._hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_BRIDGING_STATE) + + def setup_message(self): + """Prevent print of pyhap setup message to terminal.""" + pass + + def add_paired_client(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + super().add_paired_client(client_uuid, client_public) + dismiss_setup_message(self._hass) + + def remove_paired_client(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().remove_paired_client(client_uuid) + show_setup_message(self, self._hass) + + +class HomeDriver(AccessoryDriver): + """Adapter class for AccessoryDriver.""" + + def __init__(self, *args, **kwargs): + """Initialize a AccessoryDriver object.""" + super().__init__(*args, **kwargs) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 73dfbf69049..a45c8298b78 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,31 +1,67 @@ """Constants used be the HomeKit component.""" +# #### MISC #### +DOMAIN = 'homekit' +HOMEKIT_FILE = '.homekit.state' +HOMEKIT_NOTIFY_ID = 4663548 + +# #### CONFIG #### +CONF_AUTO_START = 'auto_start' +CONF_ENTITY_CONFIG = 'entity_config' +CONF_FILTER = 'filter' + +# #### CONFIG DEFAULTS #### +DEFAULT_AUTO_START = True +DEFAULT_PORT = 51827 + +# #### HOMEKIT COMPONENT SERVICES #### +SERVICE_HOMEKIT_START = 'start' + +# #### STRING CONSTANTS #### +ACCESSORY_MODEL = 'homekit.accessory' +ACCESSORY_NAME = 'Home Accessory' +BRIDGE_MODEL = 'homekit.bridge' +BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' -# Services +# #### Categories #### +CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_SENSOR = 'SENSOR' + + +# #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' +SERV_HUMIDITY_SENSOR = 'HumiditySensor' +# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, +# StatusLowBattery, Name +SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# Characteristics + +# #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' +CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' +CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' -CHAR_ON = 'On' +CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' +CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' @@ -33,5 +69,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' -# Properties +# #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/sensors.py deleted file mode 100644 index 40f97ae3ef7..00000000000 --- a/homeassistant/components/homekit/sensors.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Class to hold all sensor accessories.""" -import logging - -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.helpers.event import async_track_state_change - -from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, override_properties) -from .const import ( - SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) - - -_LOGGER = logging.getLogger(__name__) - - -def calc_temperature(state, unit=TEMP_CELSIUS): - """Calculate temperature from state and unit. - - Always return temperature as Celsius value. - Conversion is handled on the device. - """ - try: - value = float(state) - except ValueError: - return None - - return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value - - -@TYPES.register('TemperatureSensor') -class TemperatureSensor(HomeAccessory): - """Generate a TemperatureSensor accessory for a temperature sensor. - - Sensor entity must return temperature in °C, °F. - """ - - def __init__(self, hass, entity_id, display_name): - """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR') - - self._hass = hass - self._entity_id = entity_id - - self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = self.serv_temp. \ - get_characteristic(CHAR_CURRENT_TEMPERATURE) - override_properties(self.char_temp, PROP_CELSIUS) - self.char_temp.value = 0 - self.unit = None - - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_temperature(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_temperature) - - def update_temperature(self, entity_id=None, old_state=None, - new_state=None): - """Update temperature after state changed.""" - if new_state is None: - return - - unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - temperature = calc_temperature(new_state.state, unit) - if temperature is not None: - self.char_temp.set_value(temperature) - _LOGGER.debug("%s: Current temperature set to %d°C", - self._entity_id, temperature) diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml new file mode 100644 index 00000000000..e30e71301b3 --- /dev/null +++ b/homeassistant/components/homekit/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available HomeKit services + +start: + description: Starts the HomeKit component driver. diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/type_covers.py similarity index 61% rename from homeassistant/components/homekit/covers.py rename to homeassistant/components/homekit/type_covers.py index 47713f6c630..7616ef05fdf 100644 --- a/homeassistant/components/homekit/covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.cover import ATTR_CURRENT_POSITION -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -14,16 +13,17 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -@TYPES.register('Window') -class Window(HomeAccessory): +@TYPES.register('WindowCovering') +class WindowCovering(HomeAccessory): """Generate a Window accessory for a cover entity. The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name): - """Initialize a Window accessory object.""" - super().__init__(display_name, entity_id, 'WINDOW') + def __init__(self, hass, entity_id, display_name, *args, **kwargs): + """Initialize a WindowCovering accessory object.""" + super().__init__(display_name, entity_id, 'WINDOW_COVERING', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -31,12 +31,12 @@ class Window(HomeAccessory): self.current_position = None self.homekit_target = None - self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = self.serv_cover. \ + serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) + self.char_current_position = serv_cover. \ get_characteristic(CHAR_CURRENT_POSITION) - self.char_target_position = self.serv_cover. \ + self.char_target_position = serv_cover. \ get_characteristic(CHAR_TARGET_POSITION) - self.char_position_state = self.serv_cover. \ + self.char_position_state = serv_cover. \ get_characteristic(CHAR_POSITION_STATE) self.char_current_position.value = 0 self.char_target_position.value = 0 @@ -44,36 +44,28 @@ class Window(HomeAccessory): self.char_target_position.setter_callback = self.move_cover - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_cover_position(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_cover_position) - def move_cover(self, value): """Move cover to value if call came from HomeKit.""" + self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: - _LOGGER.debug("%s: Set position to %d", self._entity_id, value) + _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value if value > self.current_position: self.char_position_state.set_value(1) elif value < self.current_position: self.char_position_state.set_value(0) - self._hass.services.call( - 'cover', 'set_cover_position', - {'entity_id': self._entity_id, 'position': value}) + self._hass.components.cover.set_cover_position( + value, self._entity_id) - def update_cover_position(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" if new_state is None: return - current_position = new_state.attributes[ATTR_CURRENT_POSITION] + current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if current_position is None: return + self.current_position = int(current_position) self.char_current_position.set_value(self.current_position) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py new file mode 100644 index 00000000000..d88e7100131 --- /dev/null +++ b/homeassistant/components/homekit/type_lights.py @@ -0,0 +1,153 @@ +"""Class to hold all light accessories.""" +import logging + +from homeassistant.components.light import ( + ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LIGHT, SERV_LIGHTBULB, + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + +_LOGGER = logging.getLogger(__name__) + +RGB_COLOR = 'rgb_color' + + +@TYPES.register('Light') +class Light(HomeAccessory): + """Generate a Light accessory for a light entity. + + Currently supports: state, brightness, rgb_color. + """ + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a new Light accessory object.""" + super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, + CHAR_HUE: False, CHAR_SATURATION: False, + RGB_COLOR: False} + self._state = 0 + + self.chars = [] + self._features = self._hass.states.get(self._entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if self._features & SUPPORT_BRIGHTNESS: + self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_COLOR: + self.chars.append(CHAR_HUE) + self.chars.append(CHAR_SATURATION) + self._hue = None + self._saturation = None + + serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.get_characteristic(CHAR_ON) + self.char_on.setter_callback = self.set_state + self.char_on.value = self._state + + if CHAR_BRIGHTNESS in self.chars: + self.char_brightness = serv_light \ + .get_characteristic(CHAR_BRIGHTNESS) + self.char_brightness.setter_callback = self.set_brightness + self.char_brightness.value = 0 + if CHAR_HUE in self.chars: + self.char_hue = serv_light.get_characteristic(CHAR_HUE) + self.char_hue.setter_callback = self.set_hue + self.char_hue.value = 0 + if CHAR_SATURATION in self.chars: + self.char_saturation = serv_light \ + .get_characteristic(CHAR_SATURATION) + self.char_saturation.setter_callback = self.set_saturation + self.char_saturation.value = 75 + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._state == value: + return + + _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + self._flag[CHAR_ON] = True + self.char_on.set_value(value, should_callback=False) + + if value == 1: + self._hass.components.light.turn_on(self._entity_id) + elif value == 0: + self._hass.components.light.turn_off(self._entity_id) + + def set_brightness(self, value): + """Set brightness if call came from HomeKit.""" + _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + self._flag[CHAR_BRIGHTNESS] = True + self.char_brightness.set_value(value, should_callback=False) + if value != 0: + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + else: + self._hass.components.light.turn_off(self._entity_id) + + def set_saturation(self, value): + """Set saturation if call came from HomeKit.""" + _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + self._flag[CHAR_SATURATION] = True + self.char_saturation.set_value(value, should_callback=False) + self._saturation = value + self.set_color() + + def set_hue(self, value): + """Set hue if call came from HomeKit.""" + _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + self._flag[CHAR_HUE] = True + self.char_hue.set_value(value, should_callback=False) + self._hue = value + self.set_color() + + def set_color(self): + """Set color if call came from HomeKit.""" + # Handle Color + if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ + self._flag[CHAR_SATURATION]: + color = (self._hue, self._saturation) + _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color) + self._flag.update({ + CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) + self._hass.components.light.turn_on( + self._entity_id, hs_color=color) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update light after state change.""" + if not new_state: + return + + # Handle State + state = new_state.state + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ON] and self.char_on.value != self._state: + self.char_on.set_value(self._state, should_callback=False) + self._flag[CHAR_ON] = False + + # Handle Brightness + if CHAR_BRIGHTNESS in self.chars: + brightness = new_state.attributes.get(ATTR_BRIGHTNESS) + if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + brightness = round(brightness / 255 * 100, 0) + if self.char_brightness.value != brightness: + self.char_brightness.set_value(brightness, + should_callback=False) + self._flag[CHAR_BRIGHTNESS] = False + + # Handle Color + if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: + hue, saturation = new_state.attributes.get( + ATTR_HS_COLOR, (None, None)) + if not self._flag[RGB_COLOR] and ( + hue != self._hue or saturation != self._saturation): + self.char_hue.set_value(hue, should_callback=False) + self.char_saturation.set_value(saturation, + should_callback=False) + self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/security_systems.py b/homeassistant/components/homekit/type_security_systems.py similarity index 70% rename from homeassistant/components/homekit/security_systems.py rename to homeassistant/components/homekit/type_security_systems.py index 1b8f0a6820b..b23522f0ea2 100644 --- a/homeassistant/components/homekit/security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,7 +5,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -28,9 +27,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, alarm_code=None): + def __init__(self, hass, entity_id, display_name, + alarm_code, *args, **kwargs): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, 'ALARM_SYSTEM') + super().__init__(display_name, entity_id, 'ALARM_SYSTEM', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -38,39 +39,31 @@ class SecuritySystem(HomeAccessory): self.flag_target_state = False - self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = self.service_alarm. \ + serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm. \ get_characteristic(CHAR_CURRENT_SECURITY_STATE) self.char_current_state.value = 3 - self.char_target_state = self.service_alarm. \ + self.char_target_state = serv_alarm. \ get_characteristic(CHAR_TARGET_SECURITY_STATE) self.char_target_state.value = 3 self.char_target_state.setter_callback = self.set_security_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_security_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_security_state) - def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set security state to %d", + _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True + self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] params = {ATTR_ENTITY_ID: self._entity_id} - if self._alarm_code is not None: + if self._alarm_code: params[ATTR_CODE] = self._alarm_code self._hass.services.call('alarm_control_panel', service, params) - def update_security_state(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return @@ -78,15 +71,15 @@ class SecuritySystem(HomeAccessory): hass_state = new_state.state if hass_state not in HASS_TO_HOMEKIT: return + current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state) - _LOGGER.debug("%s: Updated current state to %s (%d)", - self._entity_id, hass_state, - current_security_state) + self.char_current_state.set_value(current_security_state, + should_callback=False) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self._entity_id, hass_state, current_security_state) if not self.flag_target_state: self.char_target_state.set_value(current_security_state, should_callback=False) - elif self.char_target_state.get_value() \ - == self.char_current_state.get_value(): + if self.char_target_state.value == self.char_current_state.value: self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py new file mode 100644 index 00000000000..e980ce4a316 --- /dev/null +++ b/homeassistant/components/homekit/type_sensors.py @@ -0,0 +1,78 @@ +"""Class to hold all sensor accessories.""" +import logging + +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + +from . import TYPES +from .accessories import ( + HomeAccessory, add_preload_service, override_properties) +from .const import ( + CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) +from .util import convert_to_float, temperature_to_homekit + + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('TemperatureSensor') +class TemperatureSensor(HomeAccessory): + """Generate a TemperatureSensor accessory for a temperature sensor. + + Sensor entity must return temperature in °C, °F. + """ + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a TemperatureSensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + + serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) + override_properties(self.char_temp, PROP_CELSIUS) + self.char_temp.value = 0 + self.unit = None + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update temperature after state changed.""" + if new_state is None: + return + + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + temperature = convert_to_float(new_state.state) + if temperature: + temperature = temperature_to_homekit(temperature, unit) + self.char_temp.set_value(temperature, should_callback=False) + _LOGGER.debug('%s: Current temperature set to %d°C', + self._entity_id, temperature) + + +@TYPES.register('HumiditySensor') +class HumiditySensor(HomeAccessory): + """Generate a HumiditySensor accessory as humidity sensor.""" + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a HumiditySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + + serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity \ + .get_characteristic(CHAR_CURRENT_HUMIDITY) + self.char_humidity.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + humidity = convert_to_float(new_state.state) + if humidity: + self.char_humidity.set_value(humidity, should_callback=False) + _LOGGER.debug('%s: Percent set to %d%%', + self._entity_id, humidity) diff --git a/homeassistant/components/homekit/switches.py b/homeassistant/components/homekit/type_switches.py similarity index 59% rename from homeassistant/components/homekit/switches.py rename to homeassistant/components/homekit/type_switches.py index 876b3406d28..1f19893d0be 100644 --- a/homeassistant/components/homekit/switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,9 +1,9 @@ """Class to hold all switch accessories.""" import logging -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, 'SWITCH') + super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -26,25 +26,18 @@ class Switch(HomeAccessory): self.flag_target_state = False - self.service_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = self.service_switch.get_characteristic(CHAR_ON) + serv_switch = add_preload_service(self, SERV_SWITCH) + self.char_on = serv_switch.get_characteristic(CHAR_ON) self.char_on.value = False self.char_on.setter_callback = self.set_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_state) - def set_state(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set switch state to %s", + _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True - service = 'turn_on' if value else 'turn_off' + self.char_on.set_value(value, should_callback=False) + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) @@ -53,10 +46,10 @@ class Switch(HomeAccessory): if new_state is None: return - current_state = (new_state.state == 'on') + current_state = (new_state.state == STATE_ON) if not self.flag_target_state: - _LOGGER.debug("%s: Set current state to %s", + _LOGGER.debug('%s: Set current state to %s', self._entity_id, current_state) self.char_on.set_value(current_state, should_callback=False) - else: - self.flag_target_state = False + + self.flag_target_state = False diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/type_thermostats.py similarity index 62% rename from homeassistant/components/homekit/thermostats.py rename to homeassistant/components/homekit/type_thermostats.py index 6d342273e8d..d49c1ca626b 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,9 +7,7 @@ from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.helpers.event import async_track_state_change + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -18,6 +16,7 @@ from .const import ( CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) +from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -33,61 +32,63 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, support_auto=False): + def __init__(self, hass, entity_id, display_name, + support_auto, *args, **kwargs): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, 'THERMOSTAT') + super().__init__(display_name, entity_id, 'THERMOSTAT', + *args, **kwargs) self._hass = hass self._entity_id = entity_id self._call_timer = None + self._unit = TEMP_CELSIUS self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False - extra_chars = None # Add additional characteristics if auto mode is supported - if support_auto: - extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE, - CHAR_HEATING_THRESHOLD_TEMPERATURE] + extra_chars = [ + CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None # Preload the thermostat service - self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT, - extra_chars) + serv_thermostat = add_preload_service(self, SERV_THERMOSTAT, + extra_chars) # Current and target mode characteristics - self.char_current_heat_cool = self.service_thermostat. \ + self.char_current_heat_cool = serv_thermostat. \ get_characteristic(CHAR_CURRENT_HEATING_COOLING) self.char_current_heat_cool.value = 0 - self.char_target_heat_cool = self.service_thermostat. \ + self.char_target_heat_cool = serv_thermostat. \ get_characteristic(CHAR_TARGET_HEATING_COOLING) self.char_target_heat_cool.value = 0 self.char_target_heat_cool.setter_callback = self.set_heat_cool # Current and target temperature characteristics - self.char_current_temp = self.service_thermostat. \ + self.char_current_temp = serv_thermostat. \ get_characteristic(CHAR_CURRENT_TEMPERATURE) self.char_current_temp.value = 21.0 - self.char_target_temp = self.service_thermostat. \ + self.char_target_temp = serv_thermostat. \ get_characteristic(CHAR_TARGET_TEMPERATURE) self.char_target_temp.value = 21.0 self.char_target_temp.setter_callback = self.set_target_temperature # Display units characteristic - self.char_display_units = self.service_thermostat. \ + self.char_display_units = serv_thermostat. \ get_characteristic(CHAR_TEMP_DISPLAY_UNITS) self.char_display_units.value = 0 # If the device supports it: high and low temperature characteristics if support_auto: - self.char_cooling_thresh_temp = self.service_thermostat. \ + self.char_cooling_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE) self.char_cooling_thresh_temp.value = 23.0 self.char_cooling_thresh_temp.setter_callback = \ self.set_cooling_threshold - self.char_heating_thresh_temp = self.service_thermostat. \ + self.char_heating_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE) self.char_heating_thresh_temp.value = 19.0 self.char_heating_thresh_temp.setter_callback = \ @@ -96,132 +97,127 @@ class Thermostat(HomeAccessory): self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_thermostat(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_thermostat) - def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" + self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value) + _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] - self._hass.services.call('climate', 'set_operation_mode', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_OPERATION_MODE: hass_value}) + self._hass.components.climate.set_operation_mode( + operation_mode=hass_value, entity_id=self._entity_id) def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set cooling threshold temperature to %.2f", + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self._entity_id, value) self.coolingthresh_flag_target_state = True - low = self.char_heating_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_HIGH: value, - ATTR_TARGET_TEMP_LOW: low}) + self.char_cooling_thresh_temp.set_value(value, should_callback=False) + low = self.char_heating_thresh_temp.value + low = temperature_to_states(low, self._unit) + value = temperature_to_states(value, self._unit) + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=value, + target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set heating threshold temperature to %.2f", + _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self._entity_id, value) self.heatingthresh_flag_target_state = True + self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time - high = self.char_cooling_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_LOW: value, - ATTR_TARGET_TEMP_HIGH: high}) + high = self.char_cooling_thresh_temp.value + high = temperature_to_states(high, self._unit) + value = temperature_to_states(value, self._unit) + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=high, + target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set target temperature to %.2f", + _LOGGER.debug('%s: Set target temperature to %.2f°C', self._entity_id, value) self.temperature_flag_target_state = True - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TEMPERATURE: value}) + self.char_target_temp.set_value(value, should_callback=False) + value = temperature_to_states(value, self._unit) + self._hass.components.climate.set_temperature( + temperature=value, entity_id=self._entity_id) - def update_thermostat(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return + self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS) + # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): + current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): + target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) - else: - self.temperature_flag_target_state = False + self.temperature_flag_target_state = False # Update cooling threshold temperature if characteristic exists - if self.char_cooling_thresh_temp is not None: + if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh is not None: + if isinstance(cooling_thresh, (int, float)): + cooling_thresh = temperature_to_homekit(cooling_thresh, + self._unit) if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) - else: - self.coolingthresh_flag_target_state = False + self.coolingthresh_flag_target_state = False # Update heating threshold temperature if characteristic exists - if self.char_heating_thresh_temp is not None: + if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh is not None: + if isinstance(heating_thresh, (int, float)): + heating_thresh = temperature_to_homekit(heating_thresh, + self._unit) if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) - else: - self.heatingthresh_flag_target_state = False + self.heatingthresh_flag_target_state = False # Update display units - display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units is not None \ - and display_units in UNIT_HASS_TO_HOMEKIT: - self.char_display_units.set_value( - UNIT_HASS_TO_HOMEKIT[display_units]) + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode is not None \ + if operation_mode \ and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) - else: - self.heat_cool_flag_target_state = False + self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode if operation_mode == STATE_HEAT: - if current_temp < target_temp: + if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: current_operation_mode = STATE_OFF elif operation_mode == STATE_COOL: - if current_temp > target_temp: + if isinstance(target_temp, float) and current_temp > target_temp: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF elif operation_mode == STATE_AUTO: # Check if auto is supported - if self.char_cooling_thresh_temp is not None: - lower_temp = self.char_heating_thresh_temp.get_value() - upper_temp = self.char_cooling_thresh_temp.get_value() + if self.char_cooling_thresh_temp: + lower_temp = self.char_heating_thresh_temp.value + upper_temp = self.char_cooling_thresh_temp.value if current_temp < lower_temp: current_operation_mode = STATE_HEAT elif current_temp > upper_temp: @@ -232,9 +228,11 @@ class Thermostat(HomeAccessory): # Check if heating or cooling are supported heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST] cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST] - if current_temp < target_temp and heat: + if isinstance(target_temp, float) and \ + current_temp < target_temp and heat: current_operation_mode = STATE_HEAT - elif current_temp > target_temp and cool: + elif isinstance(target_temp, float) and \ + current_temp > target_temp and cool: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py new file mode 100644 index 00000000000..2fa2ebd396a --- /dev/null +++ b/homeassistant/components/homekit/util.py @@ -0,0 +1,65 @@ +"""Collection of useful functions for the HomeKit component.""" +import logging + +import voluptuous as vol + +from homeassistant.core import split_entity_id +from homeassistant.const import ( + ATTR_CODE, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.temperature as temp_util +from .const import HOMEKIT_NOTIFY_ID + +_LOGGER = logging.getLogger(__name__) + + +def validate_entity_config(values): + """Validate config entry for CONF_ENTITY.""" + entities = {} + for key, config in values.items(): + entity = cv.entity_id(key) + params = {} + if not isinstance(config, dict): + raise vol.Invalid('The configuration for "{}" must be ' + ' an dictionary.'.format(entity)) + + domain, _ = split_entity_id(entity) + + if domain == 'alarm_control_panel': + code = config.get(ATTR_CODE) + params[ATTR_CODE] = cv.string(code) if code else None + + entities[entity] = params + return entities + + +def show_setup_message(bridge, hass): + """Display persistent notification with setup information.""" + pin = bridge.pincode.decode() + message = 'To setup Home Assistant in the Home App, enter the ' \ + 'following code:\n### {}'.format(pin) + hass.components.persistent_notification.create( + message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID) + + +def dismiss_setup_message(hass): + """Dismiss persistent notification and remove QR code.""" + hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + + +def convert_to_float(state): + """Return float of state, catch errors.""" + try: + return float(state) + except (ValueError, TypeError): + return None + + +def temperature_to_homekit(temperature, unit): + """Convert temperature to Celsius for HomeKit.""" + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + + +def temperature_to_states(temperature, unit): + """Convert temperature back from Celsius to Home Assistant unit.""" + return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 38ce712b9b0..c542cd9e88e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.39'] +REQUIREMENTS = ['pyhomematic==0.1.40'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) @@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' +DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' @@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -68,7 +69,7 @@ HM_DEVICE_TYPES = { 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', - 'IPSmoke', 'RFSiren', 'PresenceIP'], + 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -78,7 +79,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -86,6 +88,10 @@ HM_IGNORE_DISCOVERY_NODE = [ 'ACTUAL_HUMIDITY' ] +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + 'ACTUAL_TEMPERATURE': ['IPAreaThermostat'], +} + HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], @@ -460,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args): ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE)): + ('climate', DISCOVER_CLIMATE), + ('lock', DISCOVER_LOCKS)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) @@ -505,7 +512,8 @@ def _get_devices(hass, discovery_type, keys, interface): # Generate options for 1...n elements with 1...n parameters for param, channels in metadata.items(): - if param in HM_IGNORE_DISCOVERY_NODE: + if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \ + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []): continue # Add devices diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py new file mode 100644 index 00000000000..180d6943d8a --- /dev/null +++ b/homeassistant/components/homematicip_cloud.py @@ -0,0 +1,176 @@ +""" +Support for HomematicIP components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip_cloud/ +""" + +import logging +from socket import timeout + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import (dispatcher_send, + async_dispatcher_connect) +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['homematicip==0.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'homematicip_cloud' + +CONF_NAME = 'name' +CONF_ACCESSPOINT = 'accesspoint' +CONF_AUTHTOKEN = 'authtoken' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): [vol.Schema({ + vol.Optional(CONF_NAME, default=''): cv.string, + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + })], +}, extra=vol.ALLOW_EXTRA) + +EVENT_HOME_CHANGED = 'homematicip_home_changed' +EVENT_DEVICE_CHANGED = 'homematicip_device_changed' +EVENT_GROUP_CHANGED = 'homematicip_group_changed' +EVENT_SECURITY_CHANGED = 'homematicip_security_changed' +EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' + +ATTR_HOME_ID = 'home_id' +ATTR_HOME_LABEL = 'home_label' +ATTR_DEVICE_ID = 'device_id' +ATTR_DEVICE_LABEL = 'device_label' +ATTR_STATUS_UPDATE = 'status_update' +ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_LOW_BATTERY = 'low_battery' +ATTR_SABOTAGE = 'sabotage' +ATTR_RSSI = 'rssi' +ATTR_TYPE = 'type' + + +def setup(hass, config): + """Set up the HomematicIP component.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.home import Home + + hass.data.setdefault(DOMAIN, {}) + homes = hass.data[DOMAIN] + accesspoints = config.get(DOMAIN, []) + + def _update_event(events): + """Handle incoming HomeMaticIP events.""" + for event in events: + etype = event['eventType'] + edata = event['data'] + if etype == 'DEVICE_CHANGED': + dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) + elif etype == 'GROUP_CHANGED': + dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) + elif etype == 'HOME_CHANGED': + dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) + elif etype == 'JOURNAL_CHANGED': + dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) + return True + + for device in accesspoints: + name = device.get(CONF_NAME) + accesspoint = device.get(CONF_ACCESSPOINT) + authtoken = device.get(CONF_AUTHTOKEN) + + home = Home() + if name.lower() == 'none': + name = '' + home.label = name + try: + home.set_auth_token(authtoken) + home.init(accesspoint) + if home.get_current_state(): + _LOGGER.info("Connection to HMIP established") + else: + _LOGGER.warning("Connection to HMIP could not be established") + return False + except timeout: + _LOGGER.warning("Connection to HMIP could not be established") + return False + homes[home.id] = home + home.onEvent += _update_event + home.enable_events() + _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) + + for component in ['sensor']: + load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + + return True + + +class HomematicipGenericDevice(Entity): + """Representation of an HomematicIP generic device.""" + + def __init__(self, home, device): + """Initialize the generic device.""" + self._home = home + self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + + @callback + def _device_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._device.id: + _LOGGER.debug('Event device %s', self._device.label) + self.async_schedule_update_ha_state() + + def _name(self, addon=''): + """Return the name of the device.""" + name = '' + if self._home.label != '': + name += self._home.label + ' ' + name += self._device.label + if addon != '': + name += ' ' + addon + return name + + @property + def name(self): + """Return the name of the generic device.""" + return self._name() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Device available.""" + return not self._device.unreach + + def _generic_state_attributes(self): + """Return the state attributes of the generic device.""" + laststatus = '' + if self._device.lastStatusUpdate is not None: + laststatus = self._device.lastStatusUpdate.isoformat() + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_DEVICE_LABEL: self._device.label, + ATTR_HOME_ID: self._device.homeId, + ATTR_DEVICE_ID: self._device.id.lower(), + ATTR_STATUS_UPDATE: laststatus, + ATTR_FIRMWARE_STATE: self._device.updateState.lower(), + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + ATTR_TYPE: self._device.modelType + } + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + return self._generic_state_attributes() diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d313b5132e..17906157a6e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -4,7 +4,6 @@ This module provides WSGI application to serve the Home Assistant API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/http/ """ - from ipaddress import ip_network import logging import os @@ -32,7 +31,7 @@ from .static import ( from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .view import HomeAssistantView # noqa -REQUIREMENTS = ['aiohttp_cors==0.6.0'] +REQUIREMENTS = ['aiohttp_cors==0.7.0'] DOMAIN = 'http' diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 299a10e9f5a..81c6ea4bcfb 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -9,7 +9,7 @@ import json import logging from aiohttp import web -from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError import homeassistant.remote as rem from homeassistant.core import is_callback @@ -31,8 +31,12 @@ class HomeAssistantView(object): # pylint: disable=no-self-use def json(self, result, status_code=200, headers=None): """Return a JSON response.""" - msg = json.dumps( - result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + try: + msg = json.dumps( + result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + except TypeError as err: + _LOGGER.error('Unable to serialize to JSON: %s\n%s', err, result) + raise HTTPInternalServerError response = web.Response( body=msg, content_type=CONTENT_TYPE_JSON, status=status_code, headers=headers) diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json new file mode 100644 index 00000000000..f11af7756c7 --- /dev/null +++ b/homeassistant/components/hue/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", + "no_bridges": "Philips Hue Bridges entdeckt" + }, + "error": { + "linking": "Unbekannter Link-Fehler aufgetreten.", + "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "W\u00e4hle eine Hue Bridge" + }, + "link": { + "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Hub verbinden" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json new file mode 100644 index 00000000000..cbf63301da2 --- /dev/null +++ b/homeassistant/components/hue/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "All Philips Hue bridges are already configured", + "discover_timeout": "Unable to discover Hue bridges", + "no_bridges": "No Philips Hue bridges discovered" + }, + "error": { + "linking": "Unknown linking error occurred.", + "register_failed": "Failed to register, please try again" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Pick Hue bridge" + }, + "link": { + "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json new file mode 100644 index 00000000000..226ae8ba1f6 --- /dev/null +++ b/homeassistant/components/hue/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "error": { + "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "title": "Hue \ube0c\ub9bf\uc9c0 \uc120\ud0dd" + }, + "link": { + "description": "\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", + "title": "\ud5c8\ube0c \uc5f0\uacb0" + } + }, + "title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json new file mode 100644 index 00000000000..750ae39db12 --- /dev/null +++ b/homeassistant/components/hue/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "discover_timeout": "Hue bridges kunnen niet worden gevonden", + "no_bridges": "Geen Philips Hue bridges ontdekt" + }, + "error": { + "linking": "Er is een onbekende verbindingsfout opgetreden.", + "register_failed": "Registratie is mislukt, probeer het opnieuw" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Kies Hue bridge" + }, + "link": { + "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json new file mode 100644 index 00000000000..604475d2ff2 --- /dev/null +++ b/homeassistant/components/hue/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "discover_timeout": "Kunne ikke oppdage Hue Bridger", + "no_bridges": "Ingen Philips Hue Bridger oppdaget" + }, + "error": { + "linking": "Ukjent koblingsfeil oppstod.", + "register_failed": "Registrering feilet, vennligst pr\u00f8v igjen" + }, + "step": { + "init": { + "data": { + "host": "Vert" + }, + "title": "Velg Hue Bridge" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json new file mode 100644 index 00000000000..e364b7033a1 --- /dev/null +++ b/homeassistant/components/hue/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + }, + "error": { + "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Wybierz mostek Hue" + }, + "link": { + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", + "title": "Hub Link" + } + }, + "title": "Mostek Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json new file mode 100644 index 00000000000..91541edcc7d --- /dev/null +++ b/homeassistant/components/hue/.translations/ro.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", + "register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou" + }, + "step": { + "init": { + "data": { + "host": "Gazd\u0103" + } + }, + "link": { + "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json new file mode 100644 index 00000000000..a6c858e0e40 --- /dev/null +++ b/homeassistant/components/hue/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", + "no_bridges": "Ni odkritih mostov Philips Hue" + }, + "error": { + "linking": "Pri\u0161lo je do neznane napake pri povezavi.", + "register_failed": "Registracija ni uspela, poskusite znova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Izberite Hue most" + }, + "link": { + "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistentom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json new file mode 100644 index 00000000000..5a94e084dd2 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + }, + "error": { + "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", + "register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a" + }, + "title": "\u9009\u62e9 Hue Bridge" + }, + "link": { + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "title": "\u8fde\u63a5\u4e2d\u67a2" + } + }, + "title": "\u98de\u5229\u6d66 Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue/__init__.py similarity index 56% rename from homeassistant/components/hue.py rename to homeassistant/components/hue/__init__.py index f6e654ab44b..b70021e0304 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue/__init__.py @@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/ """ import asyncio import json -from functools import partial +import ipaddress import logging import os -import socket import async_timeout -import requests import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.discovery import SERVICE_HUE from homeassistant.const import CONF_FILENAME, CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries +from homeassistant.util.json import save_json -REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0'] +REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) @@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False PHUE_CONFIG_FILE = 'phue.conf' -CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" -DEFAULT_ALLOW_IN_EMULATED_HUE = True - CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True -BRIDGE_CONFIG_SCHEMA = vol.Schema([{ - vol.Optional(CONF_HOST): cv.string, +BRIDGE_CONFIG_SCHEMA = vol.Schema({ + # Validate as IP address and then convert back to a string. + vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE, - default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}]) +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, + vol.Optional(CONF_BRIDGES): + vol.All(cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) @@ -73,7 +70,7 @@ Press the button on the bridge to register Philips Hue with Home Assistant. """ -def setup(hass, config): +async def async_setup(hass, config): """Set up the Hue platform.""" conf = config.get(DOMAIN) if conf is None: @@ -82,196 +79,212 @@ def setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - discovery.listen( - hass, - SERVICE_HUE, - lambda service, discovery_info: - bridge_discovered(hass, service, discovery_info)) + async def async_bridge_discovered(service, discovery_info): + """Dispatcher for Hue discovery events.""" + # Ignore emulated hue + if "HASS Bridge" in discovery_info.get('name', ''): + return + + await async_setup_bridge( + hass, discovery_info['host'], + 'phue-{}.conf'.format(discovery_info['serial'])) + + discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered) # User has configured bridges if CONF_BRIDGES in conf: bridges = conf[CONF_BRIDGES] + # Component is part of config but no bridges specified, discover. elif DOMAIN in config: # discover from nupnp - hosts = requests.get(API_NUPNP).json() - bridges = [{ + websession = aiohttp_client.async_get_clientsession(hass) + + async with websession.get(API_NUPNP) as req: + hosts = await req.json() + + # Run through config schema to populate defaults + bridges = [BRIDGE_CONFIG_SCHEMA({ CONF_HOST: entry['internalipaddress'], CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - } for entry in hosts] + }) for entry in hosts] + else: # Component not specified in config, we're loaded via discovery bridges = [] - for bridge in bridges: - filename = bridge.get(CONF_FILENAME) - allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE) - allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE) - allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS) + if not bridges: + return True - host = bridge.get(CONF_HOST) - - if host is None: - host = _find_host_from_config(hass, filename) - - if host is None: - _LOGGER.error("No host found in configuration") - return False - - setup_bridge(host, hass, filename, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) + await asyncio.wait([ + async_setup_bridge( + hass, bridge[CONF_HOST], bridge[CONF_FILENAME], + bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] + ) for bridge in bridges + ]) return True -def bridge_discovered(hass, service, discovery_info): - """Dispatcher for Hue discovery events.""" - if "HASS Bridge" in discovery_info.get('name', ''): - return - - host = discovery_info.get('host') - serial = discovery_info.get('serial') - - filename = 'phue-{}.conf'.format(serial) - setup_bridge(host, hass, filename) - - -def setup_bridge(host, hass, filename=None, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True, - username=None): +async def async_setup_bridge( + hass, host, filename=None, + allow_unreachable=DEFAULT_ALLOW_UNREACHABLE, + allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS, + username=None): """Set up a given Hue bridge.""" + assert filename or username, 'Need to pass at least a username or filename' + # Only register a device once - if socket.gethostbyname(host) in hass.data[DOMAIN]: + if host in hass.data[DOMAIN]: return + if username is None: + username = await hass.async_add_job( + _find_username_from_config, hass, filename) + bridge = HueBridge(host, hass, filename, username, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) - bridge.setup() + allow_hue_groups) + await bridge.async_setup() -def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): - """Attempt to detect host based on existing configuration.""" +def _find_username_from_config(hass, filename): + """Load username from config.""" path = hass.config.path(filename) if not os.path.isfile(path): return None - try: - with open(path) as inp: - return next(iter(json.load(inp).keys())) - except (ValueError, AttributeError, StopIteration): - # ValueError if can't parse as JSON - # AttributeError if JSON value is not a dict - # StopIteration if no keys - return None + with open(path) as inp: + return list(json.load(inp).values())[0]['username'] class HueBridge(object): """Manages a single Hue bridge.""" - def __init__(self, host, hass, filename, username, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True): + def __init__(self, host, hass, filename, username, + allow_unreachable=False, allow_groups=True): """Initialize the system.""" self.host = host - self.bridge_id = socket.gethostbyname(host) self.hass = hass self.filename = filename self.username = username self.allow_unreachable = allow_unreachable - self.allow_in_emulated_hue = allow_in_emulated_hue - self.allow_hue_groups = allow_hue_groups - + self.allow_groups = allow_groups self.available = True - self.bridge = None - self.lights = {} - self.lightgroups = {} - - self.configured = False self.config_request_id = None + self.api = None - hass.data[DOMAIN][self.bridge_id] = self - - def setup(self): + async def async_setup(self): """Set up a phue bridge based on host parameter.""" - import phue + import aiohue + + api = aiohue.Bridge( + self.host, + username=self.username, + websession=aiohttp_client.async_get_clientsession(self.hass) + ) try: - kwargs = {} - if self.username is not None: - kwargs['username'] = self.username - if self.filename is not None: - kwargs['config_file_path'] = \ - self.hass.config.path(self.filename) - self.bridge = phue.Bridge(self.host, **kwargs) - except OSError: # Wrong host was given + with async_timeout.timeout(5): + # Initialize bridge and validate our username + if not self.username: + await api.create_user('home-assistant') + await api.initialize() + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): + _LOGGER.warning("Connected to Hue at %s but not registered.", + self.host) + self.async_request_configuration() + return + except (asyncio.TimeoutError, aiohue.RequestError): _LOGGER.error("Error connecting to the Hue bridge at %s", self.host) return - except phue.PhueRegistrationException: - _LOGGER.warning("Connected to Hue at %s but not registered.", - self.host) - self.request_configuration() + except aiohue.AiohueException: + _LOGGER.exception('Unknown Hue linking error occurred') + self.async_request_configuration() return except Exception: # pylint: disable=broad-except _LOGGER.exception("Unknown error connecting with Hue bridge at %s", self.host) return + self.hass.data[DOMAIN][self.host] = self + # If we came here and configuring this host, mark as done if self.config_request_id: request_id = self.config_request_id self.config_request_id = None - configurator = self.hass.components.configurator - configurator.request_done(request_id) + self.hass.components.configurator.async_request_done(request_id) - self.configured = True + self.username = api.username - discovery.load_platform( + # Save config file + await self.hass.async_add_job( + save_json, self.hass.config.path(self.filename), + {self.host: {'username': api.username}}) + + self.api = api + + self.hass.async_add_job(discovery.async_load_platform( self.hass, 'light', DOMAIN, - {'bridge_id': self.bridge_id}) + {'host': self.host})) - # create a service for calling run_scene directly on the bridge, - # used to simplify automation rules. - def hue_activate_scene(call): - """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - self.bridge.run_scene(group_name, scene_name) - - self.hass.services.register( - DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, + self.hass.services.async_register( + DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA) - def request_configuration(self): + @callback + def async_request_configuration(self): """Request configuration steps from the user.""" configurator = self.hass.components.configurator # We got an error if this method is called while we are configuring if self.config_request_id: - configurator.notify_errors( + configurator.async_notify_errors( self.config_request_id, "Failed to register, please try again.") return - self.config_request_id = configurator.request_config( - "Philips Hue", - lambda data: self.setup(), + async def config_callback(data): + """Callback for configurator data.""" + await self.async_setup() + + self.config_request_id = configurator.async_request_config( + "Philips Hue", config_callback, description=CONFIG_INSTRUCTIONS, entity_picture="/static/images/logo_philips_hue.png", submit_caption="I have pressed the button" ) - def get_api(self): - """Return the full api dictionary from phue.""" - return self.bridge.get_api() + async def hue_activate_scene(self, call, updated=False): + """Service to call directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] - def set_light(self, light_id, command): - """Adjust properties of one or more lights. See phue for details.""" - return self.bridge.set_light(light_id, command) + group = next( + (group for group in self.api.groups.values() + if group.name == group_name), None) - def set_group(self, light_id, command): - """Change light settings for a group. See phue for detail.""" - return self.bridge.set_group(light_id, command) + scene_id = next( + (scene.id for scene in self.api.scenes.values() + if scene.name == scene_name), None) + + # If we can't find it, fetch latest info. + if not updated and (group is None or scene_id is None): + await self.api.groups.update() + await self.api.scenes.update() + await self.hue_activate_scene(call, updated=True) + return + + if group is None: + _LOGGER.warning('Unable to find group %s', group_name) + return + + if scene_id is None: + _LOGGER.warning('Unable to find scene %s', scene_name) + return + + await group.set_action(scene=scene_id) @config_entries.HANDLERS.register(DOMAIN) @@ -305,12 +318,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): bridges = await discover_nupnp(websession=self._websession) except asyncio.TimeoutError: return self.async_abort( - reason='Unable to discover Hue bridges.' + reason='discover_timeout' ) if not bridges: return self.async_abort( - reason='No Philips Hue bridges discovered.' + reason='no_bridges' ) # Find already configured hosts @@ -323,7 +336,7 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): if not hosts: return self.async_abort( - reason='All Philips Hue bridges are already configured.' + reason='all_configured' ) elif len(hosts) == 1: @@ -332,7 +345,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): return self.async_show_form( step_id='init', - title='Pick Hue Bridge', data_schema=vol.Schema({ vol.Required('host'): vol.In(hosts) }) @@ -353,10 +365,10 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): await bridge.initialize() except (asyncio.TimeoutError, aiohue.RequestError, aiohue.LinkButtonNotPressed): - errors['base'] = 'Failed to register, please try again.' + errors['base'] = 'register_failed' except aiohue.AiohueException: - errors['base'] = 'Unknown linking error occurred.' - _LOGGER.exception('Uknown Hue linking error occurred') + errors['base'] = 'linking' + _LOGGER.exception('Unknown Hue linking error occurred') else: return self.async_create_entry( title=bridge.config.name, @@ -369,15 +381,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): return self.async_show_form( step_id='link', - title='Link Hub', - description=CONFIG_INSTRUCTIONS, errors=errors, ) async def async_setup_entry(hass, entry): """Set up a bridge for a config entry.""" - await hass.async_add_job(partial( - setup_bridge, entry.data['host'], hass, - username=entry.data['username'])) + await async_setup_bridge(hass, entry.data['host'], + username=entry.data['username']) return True diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json new file mode 100644 index 00000000000..59b1ecd3cd1 --- /dev/null +++ b/homeassistant/components/hue/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Philips Hue Bridge", + "step": { + "init": { + "title": "Pick Hue bridge", + "data": { + "host": "Host" + } + }, + "link": { + "title": "Link Hub", + "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" + } + }, + "error": { + "register_failed": "Failed to register, please try again", + "linking": "Unknown linking error occurred." + }, + "abort": { + "discover_timeout": "Unable to discover Hue bridges", + "no_bridges": "No Philips Hue bridges discovered", + "all_configured": "All Philips Hue bridges are already configured" + } + } +} diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 258731326ee..51f1cd42f47 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -17,7 +17,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index ce06d98bf13..227e3269628 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -17,7 +17,7 @@ from homeassistant.const import STATE_UNKNOWN, CONF_REGION from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index df58e2e9dc4..18e74966a59 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 2381e3db69e..6f5c5223ea0 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.2'] +REQUIREMENTS = ['insteonplm==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -64,19 +64,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - platform = platform_info.platform - if platform is not None: - _LOGGER.info("New INSTEON PLM device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform) + if platform_info: + platform = platform_info.platform + if platform: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - hass.async_add_job( - discovery.async_load_platform( - hass, platform, DOMAIN, - discovered={'address': device.address.hex, - 'state_key': state_key}, - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( @@ -127,13 +128,15 @@ class IPDB(object): from insteonplm.states.sensor import (VariableSensor, OnOffSensor, SmokeCO2Sensor, - IoLincSensor) + IoLincSensor, + LeakSensorDryWet) self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), + State(LeakSensorDryWet, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'), State(SmokeCO2Sensor, 'sensor'), State(OnOffSensor, 'binary_sensor'), diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a3a962a7e34..eea6c821fc0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -40,9 +40,8 @@ SUPPORT_BRIGHTNESS = 1 SUPPORT_COLOR_TEMP = 2 SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 -SUPPORT_RGB_COLOR = 16 +SUPPORT_COLOR = 16 SUPPORT_TRANSITION = 32 -SUPPORT_XY_COLOR = 64 SUPPORT_WHITE_VALUE = 128 # Integer that represents transition time in seconds to make change. @@ -51,6 +50,7 @@ ATTR_TRANSITION = "transition" # Lists holding color values ATTR_RGB_COLOR = "rgb_color" ATTR_XY_COLOR = "xy_color" +ATTR_HS_COLOR = "hs_color" ATTR_COLOR_TEMP = "color_temp" ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" @@ -86,8 +86,9 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" PROP_TO_ATTR = { 'brightness': ATTR_BRIGHTNESS, 'color_temp': ATTR_COLOR_TEMP, - 'rgb_color': ATTR_RGB_COLOR, - 'xy_color': ATTR_XY_COLOR, + 'min_mireds': ATTR_MIN_MIREDS, + 'max_mireds': ATTR_MAX_MIREDS, + 'hs_color': ATTR_HS_COLOR, 'white_value': ATTR_WHITE_VALUE, 'effect_list': ATTR_EFFECT_LIST, 'effect': ATTR_EFFECT, @@ -111,6 +112,11 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): @@ -149,13 +155,13 @@ def is_on(hass, entity_id=None): @bind_hass def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, + brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, color_temp=None, kelvin=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" hass.add_job( async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, color_temp, kelvin, white_value, + rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, profile, flash, effect, color_name) @@ -163,8 +169,9 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, @bind_hass def async_turn_on(hass, entity_id=None, transition=None, brightness=None, brightness_pct=None, rgb_color=None, xy_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): + hs_color=None, color_temp=None, kelvin=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): """Turn all or specified light on.""" data = { key: value for key, value in [ @@ -175,6 +182,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, (ATTR_BRIGHTNESS_PCT, brightness_pct), (ATTR_RGB_COLOR, rgb_color), (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), (ATTR_WHITE_VALUE, white_value), @@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params): if brightness_pct is not None: params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) + xy_color = params.pop(ATTR_XY_COLOR, None) + if xy_color is not None: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + + rgb_color = params.pop(ATTR_RGB_COLOR, None) + if rgb_color is not None: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" @@ -281,7 +297,7 @@ class SetIntentHandler(intent.IntentHandler): if 'color' in slots: intent.async_test_feature( - state, SUPPORT_RGB_COLOR, 'changing colors') + state, SUPPORT_COLOR, 'changing colors') service_data[ATTR_RGB_COLOR] = slots['color']['value'] # Use original passed in value of the color because we don't have # human readable names for that internally. @@ -428,13 +444,8 @@ class Light(ToggleEntity): return None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" + def hs_color(self): + """Return the hue and saturation color value [float, float].""" return None @property @@ -484,11 +495,16 @@ class Light(ToggleEntity): if value is not None: data[attr] = value - if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \ - ATTR_BRIGHTNESS in data: - data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB( - data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1], - data[ATTR_BRIGHTNESS]) + # Expose current color also as RGB and XY + if ATTR_HS_COLOR in data: + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB( + *data[ATTR_HS_COLOR]) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( + *data[ATTR_HS_COLOR]) + data[ATTR_HS_COLOR] = ( + round(data[ATTR_HS_COLOR][0], 3), + round(data[ATTR_HS_COLOR][1], 3), + ) return data diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py index d3e79b38647..bfea19fc3fa 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -8,8 +8,9 @@ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['abode'] @@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if (ATTR_RGB_COLOR in kwargs and + if (ATTR_HS_COLOR in kwargs and self._device.is_dimmable and self._device.has_color): - self._device.set_color(kwargs[ATTR_RGB_COLOR]) - elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: + self._device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) + + if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: self._device.set_level(kwargs[ATTR_BRIGHTNESS]) else: self._device.switch_on() @@ -68,16 +71,16 @@ class AbodeLight(AbodeDevice, Light): return self._device.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" if self._device.is_dimmable and self._device.has_color: - return self._device.color + return color_util.color_RGB_to_hs(*self._device.color) @property def supported_features(self): """Flag supported features.""" if self._device.is_dimmable and self._device.has_color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR elif self._device.is_dimmable: return SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index d6a6ef465a8..18a6b4ae266 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, + PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkstick==1.1.8'] @@ -21,7 +23,7 @@ CONF_SERIAL = 'serial' DEFAULT_NAME = 'Blinkstick' -SUPPORT_BLINKSTICK = SUPPORT_RGB_COLOR +SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERIAL): cv.string, @@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): stick = blinkstick.find_by_serial(serial) - add_devices([BlinkStickLight(stick, name)]) + add_devices([BlinkStickLight(stick, name)], True) class BlinkStickLight(Light): @@ -50,7 +52,8 @@ class BlinkStickLight(Light): self._stick = stick self._name = name self._serial = stick.get_serial() - self._rgb_color = stick.get_color() + self._hs_color = None + self._brightness = None @property def should_poll(self): @@ -63,14 +66,19 @@ class BlinkStickLight(Light): return self._name @property - def rgb_color(self): + def brightness(self): + """Read back the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def is_on(self): - """Check whether any of the LEDs colors are non-zero.""" - return sum(self._rgb_color) > 0 + """Return True if entity is on.""" + return self._brightness > 0 @property def supported_features(self): @@ -79,18 +87,24 @@ class BlinkStickLight(Light): def update(self): """Read back the device state.""" - self._rgb_color = self._stick.get_color() + rgb_color = self._stick.get_color() + hsv = color_util.color_RGB_to_hsv(*rgb_color) + self._hs_color = hsv[:2] + self._brightness = hsv[2] def turn_on(self, **kwargs): """Turn the device on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] else: - self._rgb_color = [255, 255, 255] + self._brightness = 255 - self._stick.set_color(red=self._rgb_color[0], - green=self._rgb_color[1], - blue=self._rgb_color[2]) + rgb_color = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._stick.set_color( + red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) def turn_off(self, **kwargs): """Turn the device off.""" diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index db3171cf4cf..97edd7c54d2 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkt==0.1.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'blinkt' @@ -55,7 +56,7 @@ class BlinktLight(Light): self._index = index self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -71,12 +72,9 @@ class BlinktLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -100,16 +98,17 @@ class BlinktLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] percent_bright = (self._brightness / 255) + rgb_color = color_util.color_hs_to_RGB(*self._hs_color) self._blinkt.set_pixel(self._index, - self._rgb_color[0], - self._rgb_color[1], - self._rgb_color[2], + rgb_color[0], + rgb_color[1], + rgb_color[2], percent_bright) self._blinkt.show() diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 0eef5a868b4..020f43d9935 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,23 +4,21 @@ Support for deCONZ light. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback -from homeassistant.util.color import color_RGB_to_xy +import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ light.""" if discovery_info is None: return @@ -53,14 +51,12 @@ class DeconzLight(Light): self._features |= SUPPORT_COLOR_TEMP if self._light.xy is not None: - self._features |= SUPPORT_RGB_COLOR - self._features |= SUPPORT_XY_COLOR + self._features |= SUPPORT_COLOR if self._light.effect is not None: self._features |= SUPPORT_EFFECT - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to lights events.""" self._light.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id @@ -120,22 +116,15 @@ class DeconzLight(Light): """No polling needed.""" return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on light.""" data = {'on': True} if ATTR_COLOR_TEMP in kwargs: data['ct'] = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - data['xy'] = xyb[0], xyb[1] - data['bri'] = xyb[2] - - if ATTR_XY_COLOR in kwargs: - data['xy'] = kwargs[ATTR_XY_COLOR] + if ATTR_HS_COLOR in kwargs: + data['xy'] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs: data['bri'] = kwargs[ATTR_BRIGHTNESS] @@ -157,10 +146,9 @@ class DeconzLight(Light): else: data['effect'] = 'none' - yield from self._light.async_set_state(data) + await self._light.async_set_state(data) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off light.""" data = {'on': False} @@ -176,4 +164,4 @@ class DeconzLight(Light): data['alert'] = 'lselect' del data['on'] - yield from self._light.async_set_state(data) + await self._light.async_set_state(data) diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 37a354bb3f2..ba27cbd3ac5 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -7,14 +7,13 @@ https://home-assistant.io/components/demo/ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, - Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) LIGHT_COLORS = [ - [237, 224, 33], - [255, 63, 111], + (56, 86), + (345, 75), ] LIGHT_EFFECT_LIST = ['rainbow', 'none'] @@ -22,7 +21,7 @@ LIGHT_EFFECT_LIST = ['rainbow', 'none'] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) + SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -40,17 +39,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class DemoLight(Light): """Representation of a demo light.""" - def __init__(self, unique_id, name, state, available=False, rgb=None, - ct=None, brightness=180, xy_color=(.5, .5), white=200, - effect_list=None, effect=None): + def __init__(self, unique_id, name, state, available=False, hs_color=None, + ct=None, brightness=180, white=200, effect_list=None, + effect=None): """Initialize the light.""" self._unique_id = unique_id self._name = name self._state = state - self._rgb = rgb + self._hs_color = hs_color self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness - self._xy_color = xy_color self._white = white self._effect_list = effect_list self._effect = effect @@ -84,14 +82,9 @@ class DemoLight(Light): return self._brightness @property - def xy_color(self) -> tuple: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" - return self._rgb + def hs_color(self) -> tuple: + """Return the hs color value.""" + return self._hs_color @property def color_temp(self) -> int: @@ -127,8 +120,8 @@ class DemoLight(Light): """Turn the light on.""" self._state = True - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: self._ct = kwargs[ATTR_COLOR_TEMP] @@ -136,9 +129,6 @@ class DemoLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[ATTR_XY_COLOR] - if ATTR_WHITE_VALUE in kwargs: self._white = kwargs[ATTR_WHITE_VALUE] diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 2a239c9ae10..6ffdcc0bb4a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,10 +12,11 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['flux_led==0.21'] @@ -27,7 +28,7 @@ ATTR_MODE = 'mode' DOMAIN = 'flux_led' SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR) + SUPPORT_COLOR) MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' @@ -183,9 +184,9 @@ class FluxLight(Light): return self._bulb.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._bulb.getRgb() + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -202,7 +203,13 @@ class FluxLight(Light): if not self.is_on: self._bulb.turnOn() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index b4a5e9dddfb..f9ffbb4e0bf 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -19,12 +19,11 @@ from homeassistant.const import (STATE_ON, ATTR_ENTITY_ID, CONF_NAME, from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_XY_COLOR, - SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_XY_COLOR, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION) + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, + ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, + ATTR_FLASH, ATTR_TRANSITION) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,8 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT - | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION - | SUPPORT_XY_COLOR | SUPPORT_WHITE_VALUE) + | SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION + | SUPPORT_WHITE_VALUE) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -58,8 +57,7 @@ class LightGroup(light.Light): self._is_on = False # type: bool self._available = False # type: bool self._brightness = None # type: Optional[int] - self._xy_color = None # type: Optional[Tuple[float, float]] - self._rgb_color = None # type: Optional[Tuple[int, int, int]] + self._hs_color = None # type: Optional[Tuple[float, float]] self._color_temp = None # type: Optional[int] self._min_mireds = 154 # type: Optional[int] self._max_mireds = 500 # type: Optional[int] @@ -108,14 +106,9 @@ class LightGroup(light.Light): return self._brightness @property - def xy_color(self) -> Optional[Tuple[float, float]]: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> Optional[Tuple[int, int, int]]: - """Return the RGB color value [int, int, int].""" - return self._rgb_color + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the HS color value [float, float].""" + return self._hs_color @property def color_temp(self) -> Optional[int]: @@ -164,11 +157,8 @@ class LightGroup(light.Light): if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] - - if ATTR_RGB_COLOR in kwargs: - data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -210,13 +200,8 @@ class LightGroup(light.Light): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - self._xy_color = _reduce_attribute( - on_states, ATTR_XY_COLOR, reduce=_mean_tuple) - - self._rgb_color = _reduce_attribute( - on_states, ATTR_RGB_COLOR, reduce=_mean_tuple) - if self._rgb_color is not None: - self._rgb_color = tuple(map(int, self._rgb_color)) + self._hs_color = _reduce_attribute( + on_states, ATTR_HS_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index e57bdf2c046..c4ecc5a9d2c 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -4,13 +4,13 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hive/ """ -import colorsys from homeassistant.components.hive import DATA_HIVE from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['hive'] @@ -75,10 +75,11 @@ class HiveDeviceLight(Light): return self.session.light.get_color_temp(self.node_id) @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" + def hs_color(self) -> tuple: + """Return the hs color value.""" if self.light_device_type == "colourtuneablelight": - return self.session.light.get_color(self.node_id) + rgb = self.session.light.get_color(self.node_id) + return color_util.color_RGB_to_hs(*rgb) @property def is_on(self): @@ -99,15 +100,11 @@ class HiveDeviceLight(Light): if ATTR_COLOR_TEMP in kwargs: tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) new_color_temp = round(1000000 / tmp_new_color_temp) - if ATTR_RGB_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_RGB_COLOR) - tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], - get_new_color[1], - get_new_color[2]) - hue = int(round(tmp_new_color[0] * 360)) - saturation = int(round(tmp_new_color[1] * 100)) - value = int(round((tmp_new_color[2] / 255) * 100)) - new_color = (hue, saturation, value) + if ATTR_HS_COLOR in kwargs: + get_new_color = kwargs.get(ATTR_HS_COLOR) + hue = int(get_new_color[0]) + saturation = int(get_new_color[1]) + new_color = (hue, saturation, 100) self.session.light.turn_on(self.node_id, self.light_device_type, new_brightness, new_color_temp, @@ -132,7 +129,7 @@ class HiveDeviceLight(Light): supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) elif self.light_device_type == "colourtuneablelight": supported_features = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR) return supported_features diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 661b7c2b3a1..4a54f0a337d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -8,36 +8,27 @@ import asyncio from datetime import timedelta import logging import random -import re -import socket -import voluptuous as vol +import async_timeout import homeassistant.components.hue as hue from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) -from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util as util -from homeassistant.util import yaml -import homeassistant.util.color as color_util + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, + ATTR_TRANSITION, ATTR_HS_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, + FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, + Light) +from homeassistant.util import color DEPENDENCIES = ['hue'] +SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) - SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) -SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR) +SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR) SUPPORT_HUE_EXTENDED = (SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR) SUPPORT_HUE = { @@ -48,269 +39,269 @@ SUPPORT_HUE = { 'Color temperature light': SUPPORT_HUE_COLOR_TEMP } -ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' ATTR_IS_HUE_GROUP = 'is_hue_group' - -# Legacy configuration, will be removed in 0.60 -CONF_ALLOW_UNREACHABLE = 'allow_unreachable' -DEFAULT_ALLOW_UNREACHABLE = False -CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue' -DEFAULT_ALLOW_IN_EMULATED_HUE = True -CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups' -DEFAULT_ALLOW_HUE_GROUPS = True - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_FILENAME): cv.string, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_GROUPS, - default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}) - -MIGRATION_ID = 'light_hue_config_migration' -MIGRATION_TITLE = 'Philips Hue Configuration Migration' -MIGRATION_INSTRUCTIONS = """ -Configuration for the Philips Hue component has changed; action required. - -You have configured at least one bridge: - - hue: -{config} - -This configuration is deprecated, please check the -[Hue component](https://home-assistant.io/components/hue/) page for more -information. -""" - -SIGNAL_CALLBACK = 'hue_light_callback_{}_{}' +# Minimum Hue Bridge API version to support groups +# 1.4.0 introduced extended group info +# 1.12 introduced the state object for groups +# 1.13 introduced "any_on" to group state objects +GROUP_MIN_API_VERSION = (1, 13, 0) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Hue lights.""" - if discovery_info is None or 'bridge_id' not in discovery_info: + if discovery_info is None: return - if config is not None and config: - # Legacy configuration, will be removed in 0.60 - config_str = yaml.dump([config]) - # Indent so it renders in a fixed-width font - config_str = re.sub('(?m)^', ' ', config_str) - hass.components.persistent_notification.async_create( - MIGRATION_INSTRUCTIONS.format(config=config_str), - title=MIGRATION_TITLE, - notification_id=MIGRATION_ID) + bridge = hass.data[hue.DOMAIN][discovery_info['host']] + cur_lights = {} + cur_groups = {} - bridge_id = discovery_info['bridge_id'] - bridge = hass.data[hue.DOMAIN][bridge_id] - unthrottled_update_lights(hass, bridge, add_devices) + api_version = tuple( + int(v) for v in bridge.api.config.apiversion.split('.')) + + allow_groups = bridge.allow_groups + if allow_groups and api_version < GROUP_MIN_API_VERSION: + _LOGGER.warning('Please update your Hue bridge to support groups') + allow_groups = False + + # Hue updates all lights via a single API call. + # + # If we call a service to update 2 lights, we only want the API to be + # called once. + # + # The throttle decorator will return right away if a call is currently + # in progress. This means that if we are updating 2 lights, the first one + # is in the update method, the second one will skip it and assume the + # update went through and updates it's data, not good! + # + # The current mechanism will make sure that all lights will wait till + # the update call is done before writing their data to the state machine. + # + # An alternative approach would be to disable automatic polling by Home + # Assistant and take control ourselves. This works great for polling as now + # we trigger from 1 time update an update to all entities. However it gets + # tricky from inside async_turn_on and async_turn_off. + # + # If automatic polling is enabled, Home Assistant will call the entity + # update method after it is done calling all the services. This means that + # when we update, we know all commands have been processed. If we trigger + # the update from inside async_turn_on, the update will not capture the + # changes to the second entity until the next polling update because the + # throttle decorator will prevent the call. + + progress = None + light_progress = set() + group_progress = set() + + async def request_update(is_group, object_id): + """Request an update. + + We will only make 1 request to the server for updating at a time. If a + request is in progress, we will join the request that is in progress. + + This approach is possible because should_poll=True. That means that + Home Assistant will ask lights for updates during a polling cycle or + after it has called a service. + + We keep track of the lights that are waiting for the request to finish. + When new data comes in, we'll trigger an update for all non-waiting + lights. This covers the case where a service is called to enable 2 + lights but in the meanwhile some other light has changed too. + """ + nonlocal progress + + progress_set = group_progress if is_group else light_progress + progress_set.add(object_id) + + if progress is not None: + return await progress + + progress = asyncio.ensure_future(update_bridge()) + result = await progress + progress = None + light_progress.clear() + group_progress.clear() + return result + + async def update_bridge(): + """Update the values of the bridge. + + Will update lights and, if enabled, groups from the bridge. + """ + tasks = [] + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + False, cur_lights, light_progress + )) + + if allow_groups: + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + True, cur_groups, group_progress + )) + + await asyncio.wait(tasks) + + await update_bridge() -@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) -def update_lights(hass, bridge, add_devices): - """Update the Hue light objects with latest info from the bridge.""" - return unthrottled_update_lights(hass, bridge, add_devices) +async def async_update_items(hass, bridge, async_add_devices, + request_bridge_update, is_group, current, + progress_waiting): + """Update either groups or lights from the bridge.""" + import aiohue - -def unthrottled_update_lights(hass, bridge, add_devices): - """Update the lights (Internal version of update_lights).""" - import phue - - if not bridge.configured: - return + if is_group: + api = bridge.api.groups + else: + api = bridge.api.lights try: - api = bridge.get_api() - except phue.PhueRequestTimeout: - _LOGGER.warning("Timeout trying to reach the bridge") - bridge.available = False - return - except ConnectionRefusedError: - _LOGGER.error("The bridge refused the connection") - bridge.available = False - return - except socket.error: - # socket.error when we cannot reach Hue - _LOGGER.exception("Cannot reach the bridge") + with async_timeout.timeout(4): + await api.update() + except (asyncio.TimeoutError, aiohue.AiohueException): + if not bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s', bridge.host) bridge.available = False + + for light_id, light in current.items(): + if light_id not in progress_waiting: + light.async_schedule_update_ha_state() + return - bridge.available = True + if not bridge.available: + _LOGGER.info('Reconnected to bridge %s', bridge.host) + bridge.available = True - new_lights = process_lights( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - if bridge.allow_hue_groups: - new_lightgroups = process_groups( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - new_lights.extend(new_lightgroups) + new_lights = [] + + for item_id in api: + if item_id not in current: + current[item_id] = HueLight( + api[item_id], request_bridge_update, bridge, is_group) + + new_lights.append(current[item_id]) + elif item_id not in progress_waiting: + current[item_id].async_schedule_update_ha_state() if new_lights: - add_devices(new_lights) - - -def process_lights(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all lights.""" - api_lights = api.get('lights') - - if not isinstance(api_lights, dict): - _LOGGER.error("Got unexpected result from Hue API") - return [] - - new_lights = [] - - for light_id, info in api_lights.items(): - if light_id not in bridge.lights: - bridge.lights[light_id] = HueLight( - int(light_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue) - new_lights.append(bridge.lights[light_id]) - else: - bridge.lights[light_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lights[light_id].light_id)) - - return new_lights - - -def process_groups(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all groups.""" - api_groups = api.get('groups') - - if not isinstance(api_groups, dict): - _LOGGER.error('Got unexpected result from Hue API') - return [] - - new_lights = [] - - for lightgroup_id, info in api_groups.items(): - if 'state' not in info: - _LOGGER.warning( - "Group info does not contain state. Please update your hub") - return [] - - if lightgroup_id not in bridge.lightgroups: - bridge.lightgroups[lightgroup_id] = HueLight( - int(lightgroup_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue, True) - new_lights.append(bridge.lightgroups[lightgroup_id]) - else: - bridge.lightgroups[lightgroup_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lightgroups[lightgroup_id].light_id)) - - return new_lights + async_add_devices(new_lights) class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light_id, info, bridge, update_lights_cb, - allow_unreachable, allow_in_emulated_hue, is_group=False): + def __init__(self, light, request_bridge_update, bridge, is_group=False): """Initialize the light.""" - self.light_id = light_id - self.info = info + self.light = light + self.async_request_bridge_update = request_bridge_update self.bridge = bridge - self.update_lights = update_lights_cb - self.allow_unreachable = allow_unreachable self.is_group = is_group - self.allow_in_emulated_hue = allow_in_emulated_hue if is_group: - self._command_func = self.bridge.set_group + self.is_osram = False + self.is_philips = False else: - self._command_func = self.bridge.set_light + self.is_osram = light.manufacturername == 'OSRAM' + self.is_philips = light.manufacturername == 'Philips' @property def unique_id(self): """Return the ID of this Hue light.""" - return self.info.get('uniqueid') + return self.light.uniqueid @property def name(self): """Return the name of the Hue light.""" - return self.info.get('name', DEVICE_DEFAULT_NAME) + return self.light.name @property def brightness(self): """Return the brightness of this light between 0..255.""" if self.is_group: - return self.info['action'].get('bri') - return self.info['state'].get('bri') + return self.light.action.get('bri') + return self.light.state.get('bri') @property - def xy_color(self): - """Return the XY color value.""" + def _color_mode(self): + """Return the hue color mode.""" if self.is_group: - return self.info['action'].get('xy') - return self.info['state'].get('xy') + return self.light.action.get('colormode') + return self.light.state.get('colormode') + + @property + def hs_color(self): + """Return the hs color value.""" + # pylint: disable=redefined-outer-name + mode = self._color_mode + + if mode not in ('hs', 'xy'): + return + + source = self.light.action if self.is_group else self.light.state + + hue = source.get('hue') + sat = source.get('sat') + + # Sometimes the state will not include valid hue/sat values. + # Reported as issue 13434 + if hue is not None and sat is not None: + return hue / 65535 * 360, sat / 255 * 100 + + if 'xy' not in source: + return None + + return color.color_xy_to_hs(*source['xy']) @property def color_temp(self): """Return the CT color value.""" + # Don't return color temperature unless in color temperature mode + if self._color_mode != "ct": + return None + if self.is_group: - return self.info['action'].get('ct') - return self.info['state'].get('ct') + return self.light.action.get('ct') + return self.light.state.get('ct') @property def is_on(self): """Return true if device is on.""" if self.is_group: - return self.info['state']['any_on'] - return self.info['state']['on'] + return self.light.state['any_on'] + return self.light.state['on'] @property def available(self): """Return if light is available.""" return self.bridge.available and (self.is_group or - self.allow_unreachable or - self.info['state']['reachable']) + self.bridge.allow_unreachable or + self.light.state['reachable']) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED) + return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED) @property def effect_list(self): """Return the list of supported effects.""" return [EFFECT_COLORLOOP, EFFECT_RANDOM] - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {'on': True} if ATTR_TRANSITION in kwargs: command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) - if ATTR_XY_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': - color_hue, sat = color_util.color_xy_to_hs( - *kwargs[ATTR_XY_COLOR]) - command['hue'] = color_hue / 360 * 65535 - command['sat'] = sat / 100 * 255 - else: - command['xy'] = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': - hsv = color_util.color_RGB_to_hsv( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['hue'] = hsv[0] / 360 * 65535 - command['sat'] = hsv[1] / 100 * 255 - command['bri'] = hsv[2] / 100 * 255 - else: - xyb = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['xy'] = xyb[0], xyb[1] + if ATTR_HS_COLOR in kwargs: + command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) @@ -336,12 +327,15 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif self.info.get('manufacturername') == 'Philips': + elif self.is_philips: command['effect'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" command = {'on': False} @@ -359,27 +353,19 @@ class HueLight(Light): else: command['alert'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def update(self): + async def async_update(self): """Synchronize state with bridge.""" - self.update_lights(no_throttle=True) + await self.async_request_bridge_update(self.is_group, self.light.id) @property def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - if not self.allow_in_emulated_hue: - attributes[ATTR_EMULATED_HUE_HIDDEN] = \ - not self.allow_in_emulated_hue if self.is_group: attributes[ATTR_IS_HUE_GROUP] = self.is_group return attributes - - @asyncio.coroutine - def async_added_to_hass(self): - """Register update callback.""" - dev_id = self.bridge.bridge_id, self.light_id - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CALLBACK.format(*dev_id), - self.async_schedule_update_ha_state) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 2057192299e..8ba2329af7e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -11,10 +11,11 @@ import socket import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,7 @@ DEFAULT_EFFECT_LIST = ['HDMI', 'Cinema brighten lights', 'Cinema dim lights', 'Color traces', 'UDP multicast listener', 'UDP listener', 'X-Mas'] -SUPPORT_HYPERION = (SUPPORT_RGB_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) +SUPPORT_HYPERION = (SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -107,9 +108,9 @@ class Hyperion(Light): return self._brightness @property - def rgb_color(self): - """Return last RGB color value set.""" - return self._rgb_color + def hs_color(self): + """Return last color value set.""" + return color_util.color_RGB_to_hs(*self._rgb_color) @property def is_on(self): @@ -138,8 +139,8 @@ class Hyperion(Light): def turn_on(self, **kwargs): """Turn the lights on.""" - if ATTR_RGB_COLOR in kwargs: - rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) elif self._rgb_mem == [0, 0, 0]: rgb_color = self._default_color else: @@ -214,7 +215,7 @@ class Hyperion(Light): pass led_color = response['info']['activeLedColor'] - if not led_color or led_color[0]['RGB value'] == [0, 0, 0]: + if not led_color or led_color[0]['RGB Value'] == [0, 0, 0]: # Get the active effect if response['info'].get('activeEffects'): self._rgb_color = [175, 0, 255] @@ -233,8 +234,7 @@ class Hyperion(Light): self._effect = None else: # Get the RGB color - self._rgb_color =\ - response['info']['activeLedColor'][0]['RGB Value'] + self._rgb_color = led_color[0]['RGB Value'] self._brightness = max(self._rgb_color) self._rgb_mem = [int(round(float(x)*255/self._brightness)) for x in self._rgb_color] diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 9717993f77d..77e3972968c 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -10,8 +10,8 @@ import math import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_EFFECT, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_EFFECT, PLATFORM_SCHEMA, Light) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -27,7 +27,7 @@ DEFAULT_PORT = 8080 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) @@ -77,9 +77,9 @@ class IGloLamp(Light): self._lamp.min_kelvin)) @property - def rgb_color(self): - """Return the RGB value.""" - return self._lamp.state()['rgb'] + def hs_color(self): + """Return the hs value.""" + return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) @property def effect(self): @@ -95,7 +95,7 @@ class IGloLamp(Light): def supported_features(self): """Flag supported features.""" return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_RGB_COLOR | SUPPORT_EFFECT) + SUPPORT_COLOR | SUPPORT_EFFECT) @property def is_on(self): @@ -111,8 +111,8 @@ class IGloLamp(Light): self._lamp.brightness(brightness) return - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._lamp.rgb(*rgb) return diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 83083e34bad..18446951735 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -9,11 +9,12 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' @@ -114,15 +115,10 @@ class KNXLight(Light): None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RBG color value.""" + def hs_color(self): + """Return the HS color value.""" if self.device.supports_color: - return self.device.current_color + return color_util.color_RGB_to_hs(*self.device.current_color) return None @property @@ -157,7 +153,7 @@ class KNXLight(Light): if self.device.supports_brightness: flags |= SUPPORT_BRIGHTNESS if self.device.supports_color: - flags |= SUPPORT_RGB_COLOR + flags |= SUPPORT_COLOR return flags async def async_turn_on(self, **kwargs): @@ -165,9 +161,10 @@ class KNXLight(Light): if ATTR_BRIGHTNESS in kwargs: if self.device.supports_brightness: await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS])) - elif ATTR_RGB_COLOR in kwargs: + elif ATTR_HS_COLOR in kwargs: if self.device.supports_color: - await self.device.set_color(kwargs[ATTR_RGB_COLOR]) + await self.device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) else: await self.device.set_on() diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 0bb65a78c6e..dff5ccd42ac 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -16,10 +16,10 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, - ATTR_EFFECT, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, - DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, - SUPPORT_XY_COLOR, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -87,11 +87,22 @@ LIFX_EFFECT_SCHEMA = vol.Schema({ LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ ATTR_BRIGHTNESS: VALID_BRIGHTNESS, ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_COLOR_NAME: cv.string, - ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple)), - ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), - ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=0)), ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), ATTR_MODE: vol.In(PULSE_MODES), @@ -168,16 +179,8 @@ def find_hsbk(**kwargs): preprocess_turn_on_alternatives(kwargs) - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - brightness = int(brightness / 100 * 65535) - kelvin = 3500 - - if ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] hue = int(hue / 360 * 65535) saturation = int(saturation / 100 * 65535) kelvin = 3500 @@ -585,7 +588,7 @@ class LIFXColor(LIFXLight): def supported_features(self): """Flag supported features.""" support = super().supported_features - support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR + support |= SUPPORT_COLOR return support @property @@ -598,15 +601,12 @@ class LIFXColor(LIFXLight): ] @property - def rgb_color(self): - """Return the RGB value.""" - hue, sat, bri, _ = self.device.color - + def hs_color(self): + """Return the hs value.""" + hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - bri = bri / 65535 * 100 - - return color_util.color_hsv_to_RGB(hue, sat, bri) + return (hue, sat) class LIFXStrip(LIFXColor): diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py index cf3dba848a8..490eeb6ecab 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -7,14 +7,13 @@ not yet support Windows. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ -import colorsys import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) from homeassistant.helpers.event import track_time_change from homeassistant.util.color import ( @@ -37,7 +36,7 @@ TEMP_MAX_HASS = 500 TEMP_MIN = 2500 TEMP_MIN_HASS = 154 -SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | +SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -129,17 +128,6 @@ class LIFX(object): self._liffylights.probe(address) -def convert_rgb_to_hsv(rgb): - """Convert Home Assistant RGB values to HSV values.""" - red, green, blue = [_ / BYTE_MAX for _ in rgb] - - hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue) - - return [int(hue * SHORT_MAX), - int(saturation * SHORT_MAX), - int(brightness * SHORT_MAX)] - - class LIFXLight(Light): """Representation of a LIFX light.""" @@ -170,11 +158,9 @@ class LIFXLight(Light): return self._ip @property - def rgb_color(self): - """Return the RGB value.""" - _LOGGER.debug( - "rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2]) - return self._rgb + def hs_color(self): + """Return the hs value.""" + return (self._hue / 65535 * 360, self._sat / 65535 * 100) @property def brightness(self): @@ -209,13 +195,13 @@ class LIFXLight(Light): else: fade = 0 - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + hue = hue / 360 * 65535 + saturation = saturation / 100 * 65535 else: hue = self._hue saturation = self._sat - brightness = self._bri if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) @@ -265,16 +251,3 @@ class LIFXLight(Light): self._sat = sat self._bri = bri self._kel = kel - - red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX, - sat / SHORT_MAX, - bri / SHORT_MAX) - - red = int(red * BYTE_MAX) - green = int(green * BYTE_MAX) - blue = int(blue * BYTE_MAX) - - _LOGGER.debug("set_color: %d %d %d %d [%d %d %d]", - hue, sat, bri, kel, red, green, blue) - - self._rgb = [red, green, blue] diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index f011792a15c..bb84b3a6fed 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -12,12 +12,13 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, color_hs_to_RGB) from homeassistant.helpers.restore_state import async_get_last_state REQUIREMENTS = ['limitlessled==1.1.0'] @@ -40,19 +41,19 @@ LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led', 'dimmer'] EFFECT_NIGHT = 'night' -RGB_BOUNDARY = 40 +MIN_SATURATION = 10 -WHITE = [255, 255, 255] +WHITE = [0, 0] SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_FLASH | SUPPORT_RGB_COLOR | + SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGBWW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_BRIDGES): vol.All(cv.ensure_list, [ @@ -196,7 +197,7 @@ class LimitlessLEDGroup(Light): self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') self._temperature = last_state.attributes.get('color_temp') - self._color = last_state.attributes.get('rgb_color') + self._color = last_state.attributes.get('hs_color') @property def should_poll(self): @@ -239,7 +240,7 @@ class LimitlessLEDGroup(Light): return self._temperature @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -282,17 +283,17 @@ class LimitlessLEDGroup(Light): self._brightness = kwargs[ATTR_BRIGHTNESS] args['brightness'] = self.limitlessled_brightness() - if ATTR_RGB_COLOR in kwargs and self._supported & SUPPORT_RGB_COLOR: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and self._supported & SUPPORT_COLOR: + self._color = kwargs[ATTR_HS_COLOR] # White is a special case. - if min(self._color) > 256 - RGB_BOUNDARY: + if self._color[1] < MIN_SATURATION: pipeline.white() self._color = WHITE else: args['color'] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: - if self._supported & SUPPORT_RGB_COLOR: + if self._supported & SUPPORT_COLOR: pipeline.white() self._color = WHITE if self._supported & SUPPORT_COLOR_TEMP: @@ -333,6 +334,6 @@ class LimitlessLEDGroup(Light): return self._brightness / 255 def limitlessled_color(self): - """Convert Home Assistant RGB list to Color tuple.""" + """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(*tuple(self._color)) + return Color(*color_hs_to_RGB(*tuple(self._color))) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index f97e37127b1..a0534ba4e95 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,10 +12,9 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, Light, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, @@ -25,6 +24,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -191,14 +191,13 @@ class MqttLight(MqttAvailability, Light): self._on_command_type = on_command_type self._state = False self._brightness = None - self._rgb = None + self._hs = None self._color_temp = None self._effect = None self._white_value = None - self._xy = None self._supported_features = 0 self._supported_features |= ( - topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR) + topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_COLOR) self._supported_features |= ( topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and SUPPORT_BRIGHTNESS) @@ -212,7 +211,7 @@ class MqttLight(MqttAvailability, Light): topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and SUPPORT_WHITE_VALUE) self._supported_features |= ( - topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_XY_COLOR) + topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -263,19 +262,18 @@ class MqttLight(MqttAvailability, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - self._rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + rgb = [int(val) for val in + templates[CONF_RGB](payload).split(',')] + self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) - self._rgb = [255, 255, 255] + self._hs = (0, 0) if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - self._rgb = [255, 255, 255] - else: - self._rgb = None + self._hs = (0, 0) @callback def color_temp_received(topic, payload, qos): @@ -330,19 +328,18 @@ class MqttLight(MqttAvailability, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - self._xy = [float(val) for val in + xy_color = [float(val) for val in templates[CONF_XY](payload).split(',')] + self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) - self._xy = [1, 1] + self._hs = (0, 0) if self._topic[CONF_XY_COMMAND_TOPIC] is not None: - self._xy = [1, 1] - else: - self._xy = None + self._hs = (0, 0) @property def brightness(self): @@ -350,9 +347,9 @@ class MqttLight(MqttAvailability, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def color_temp(self): @@ -364,11 +361,6 @@ class MqttLight(MqttAvailability, Light): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the RGB color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -426,24 +418,43 @@ class MqttLight(MqttAvailability, Light): kwargs[ATTR_BRIGHTNESS] = self._brightness if \ self._brightness else 255 - if ATTR_RGB_COLOR in kwargs and \ + if ATTR_HS_COLOR in kwargs and \ self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - colors = ('red', 'green', 'blue') - variables = {key: val for key, val in - zip(colors, kwargs[ATTR_RGB_COLOR])} - rgb_color_str = tpl.async_render(variables) + rgb_color_str = tpl.async_render({ + 'red': rgb[0], + 'green': rgb[1], + 'blue': rgb[2], + }) else: - rgb_color_str = '{},{},{}'.format(*kwargs[ATTR_RGB_COLOR]) + rgb_color_str = '{},{},{}'.format(*rgb) mqtt.async_publish( self.hass, self._topic[CONF_RGB_COMMAND_TOPIC], rgb_color_str, self._qos, self._retain) if self._optimistic_rgb: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] + should_update = True + + if ATTR_HS_COLOR in kwargs and \ + self._topic[CONF_XY_COMMAND_TOPIC] is not None: + + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + mqtt.async_publish( + self.hass, self._topic[CONF_XY_COMMAND_TOPIC], + '{},{}'.format(*xy_color), self._qos, + self._retain) + + if self._optimistic_xy: + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_BRIGHTNESS in kwargs and \ @@ -493,18 +504,6 @@ class MqttLight(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs and \ - self._topic[CONF_XY_COMMAND_TOPIC] is not None: - - mqtt.async_publish( - self.hass, self._topic[CONF_XY_COMMAND_TOPIC], - '{},{}'.format(*kwargs[ATTR_XY_COLOR]), self._qos, - self._retain) - - if self._optimistic_xy: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - if self._on_command_type == 'last': mqtt.async_publish(self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'], self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 19747b89ca0..25212e45c60 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -13,10 +13,10 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -143,31 +144,26 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb: - self._rgb = [0, 0, 0] + if rgb or xy: + self._hs = [0, 0] else: - self._rgb = None + self._hs = None if white_value: self._white_value = 255 else: self._white_value = None - if xy: - self._xy = [1, 1] - else: - self._xy = None - self._flash_times = flash_times self._brightness_scale = brightness_scale self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) - self._supported_features |= (rgb and SUPPORT_RGB_COLOR) + self._supported_features |= (rgb and SUPPORT_COLOR) self._supported_features |= (brightness and SUPPORT_BRIGHTNESS) self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP) self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) - self._supported_features |= (xy and SUPPORT_XY_COLOR) + self._supported_features |= (xy and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -184,17 +180,26 @@ class MqttJson(MqttAvailability, Light): elif values['state'] == 'OFF': self._state = False - if self._rgb is not None: + if self._hs is not None: try: red = int(values['color']['r']) green = int(values['color']['g']) blue = int(values['color']['b']) - self._rgb = [red, green, blue] + self._hs = color_util.color_RGB_to_hs(red, green, blue) except KeyError: pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: + x_color = float(values['color']['x']) + y_color = float(values['color']['y']) + + self._hs = color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") if self._brightness is not None: try: @@ -230,17 +235,6 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid white value received") - if self._xy is not None: - try: - x_color = float(values['color']['x']) - y_color = float(values['color']['y']) - - self._xy = [x_color, y_color] - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -269,20 +263,15 @@ class MqttJson(MqttAvailability, Light): return self._effect_list @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def white_value(self): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the XY color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -318,15 +307,23 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_RGB_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color'] = { - 'r': kwargs[ATTR_RGB_COLOR][0], - 'g': kwargs[ATTR_RGB_COLOR][1], - 'b': kwargs[ATTR_RGB_COLOR][2] + 'r': rgb[0], + 'g': rgb[1], + 'b': rgb[2], + 'x': xy_color[0], + 'y': xy_color[1], } if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_FLASH in kwargs: @@ -370,16 +367,6 @@ class MqttJson(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs: - message['color'] = { - 'x': kwargs[ATTR_XY_COLOR][0], - 'y': kwargs[ATTR_XY_COLOR][1] - } - - if self._optimistic: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index de0f6d934c6..06a94cd23b4 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -12,15 +12,16 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, + ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -142,9 +143,9 @@ class MqttTemplate(MqttAvailability, Light): if (self._templates[CONF_RED_TEMPLATE] is not None and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None): - self._rgb = [0, 0, 0] + self._hs = [0, 0] else: - self._rgb = None + self._hs = None self._effect = None for tpl in self._templates.values(): @@ -186,17 +187,18 @@ class MqttTemplate(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid color temperature value received") - if self._rgb is not None: + if self._hs is not None: try: - self._rgb[0] = int( + red = int( self._templates[CONF_RED_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[1] = int( + green = int( self._templates[CONF_GREEN_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[2] = int( + blue = int( self._templates[CONF_BLUE_TEMPLATE]. async_render_with_possible_json_value(payload)) + self._hs = color_util.color_RGB_to_hs(red, green, blue) except ValueError: _LOGGER.warning("Invalid color value received") @@ -236,9 +238,9 @@ class MqttTemplate(MqttAvailability, Light): return self._color_temp @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -300,13 +302,18 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self._color_temp = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - values['red'] = kwargs[ATTR_RGB_COLOR][0] - values['green'] = kwargs[ATTR_RGB_COLOR][1] - values['blue'] = kwargs[ATTR_RGB_COLOR][2] + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + values['red'] = rgb[0] + values['green'] = rgb[1] + values['blue'] = rgb[2] if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_WHITE_VALUE in kwargs: values['white_value'] = int(kwargs[ATTR_WHITE_VALUE]) @@ -360,8 +367,8 @@ class MqttTemplate(MqttAvailability, Light): features = (SUPPORT_FLASH | SUPPORT_TRANSITION) if self._brightness is not None: features = features | SUPPORT_BRIGHTNESS - if self._rgb is not None: - features = features | SUPPORT_RGB_COLOR + if self._hs is not None: + features = features | SUPPORT_COLOR if self._effect_list is not None: features = features | SUPPORT_EFFECT if self._color_temp is not None: diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index a37553017e7..7aa1e754c43 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -6,13 +6,13 @@ https://home-assistant.io/components/light.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list +import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE def setup_platform(hass, config, add_devices, discovery_info=None): @@ -35,7 +35,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): super().__init__(*args) self._state = None self._brightness = None - self._rgb = None + self._hs = None self._white = None @property @@ -44,9 +44,9 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -63,11 +63,6 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): """Return true if device is on.""" return self._state - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_MYSENSORS - def _turn_on_light(self): """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -103,10 +98,14 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" - rgb = self._rgb + rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) - new_rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None: + new_rgb = color_util.color_hs_to_RGB(*hs_color) + else: + new_rgb = None new_white = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: @@ -126,7 +125,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): if self.gateway.optimistic: # optimistically assume that light has changed state - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._white = white self._values[self.value_type] = hex_color @@ -160,12 +159,17 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): color_list = rgb_hex_to_rgb_list(value) if len(color_list) > 3: self._white = color_list.pop() - self._rgb = color_list + self._hs = color_util.color_RGB_to_hs(*color_list) class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -183,6 +187,14 @@ class MySensorsLightDimmer(MySensorsLight): class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -204,6 +216,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): # pylint: disable=too-many-ancestors + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index ecb120e3079..d9312e6aadc 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -11,7 +11,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH) + SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + ATTR_HS_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN REQUIREMENTS = ['python-mystrom==0.3.8'] @@ -20,7 +21,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'myStrom bulb' -SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH) +SUPPORT_MYSTROM = ( + SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | + SUPPORT_COLOR +) EFFECT_RAINBOW = 'rainbow' EFFECT_SUNRISE = 'sunrise' @@ -67,6 +71,8 @@ class MyStromLight(Light): self._state = None self._available = False self._brightness = 0 + self._color_h = 0 + self._color_s = 0 @property def name(self): @@ -83,6 +89,11 @@ class MyStromLight(Light): """Return the brightness of the light.""" return self._brightness + @property + def hs_color(self): + """Return the color of the light.""" + return self._color_h, self._color_s + @property def available(self) -> bool: """Return True if entity is available.""" @@ -105,11 +116,21 @@ class MyStromLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) + if ATTR_HS_COLOR in kwargs: + color_h, color_s = kwargs[ATTR_HS_COLOR] + elif ATTR_BRIGHTNESS in kwargs: + # Brightness update, keep color + color_h, color_s = self._color_h, self._color_s + else: + color_h, color_s = 0, 0 # Back to white + try: if not self.is_on: self._bulb.set_on() if brightness is not None: - self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255)) + self._bulb.set_color_hsv( + int(color_h), int(color_s), round(brightness * 100 / 255) + ) if effect == EFFECT_SUNRISE: self._bulb.set_sunrise(30) if effect == EFFECT_RAINBOW: @@ -132,7 +153,14 @@ class MyStromLight(Light): try: self._state = self._bulb.get_status() - self._brightness = int(self._bulb.get_brightness()) * 255 / 100 + + colors = self._bulb.get_color()['color'] + color_h, color_s, color_v = colors.split(';') + + self._color_h = int(color_h) + self._color_s = int(color_s) + self._brightness = int(color_v) * 255 / 100 + self._available = True except MyStromConnectionError: _LOGGER.warning("myStrom bulb not online") diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index ff526c4783d..2c44620caca 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -13,15 +13,15 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) +import homeassistant.util.color as color_util REQUIREMENTS = ['lightify==1.0.6.1'] @@ -35,8 +35,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_EFFECT | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) + SUPPORT_EFFECT | SUPPORT_COLOR | + SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -113,7 +113,7 @@ class Luminary(Light): self.update_lights = update_lights self._luminary = luminary self._brightness = None - self._rgb = [None] + self._hs = None self._name = None self._temperature = None self._state = False @@ -125,9 +125,9 @@ class Luminary(Light): return self._name @property - def rgb_color(self): - """Last RGB color value set.""" - return self._rgb + def hs_color(self): + """Last hs color value set.""" + return self._hs @property def color_temp(self): @@ -158,42 +158,24 @@ class Luminary(Light): """Turn the device on.""" if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) else: transition = 0 - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", - self._name, self._brightness) self._luminary.set_luminance( int(self._brightness / 2.55), transition) else: self._luminary.set_onoff(1) - if ATTR_RGB_COLOR in kwargs: - red, green, blue = kwargs[ATTR_RGB_COLOR] - _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:" - " %s is: %s %s %s ", - self._name, red, green, blue) - self._luminary.set_rgb(red, green, blue, transition) - - if ATTR_XY_COLOR in kwargs: - x_mired, y_mired = kwargs[ATTR_XY_COLOR] - _LOGGER.debug("turn_on requested ATTR_XY_COLOR for light:" - " %s is: %s,%s", self._name, x_mired, y_mired) - red, green, blue = color_xy_brightness_to_RGB( - x_mired, y_mired, self._brightness) + if ATTR_HS_COLOR in kwargs: + red, green, blue = \ + color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._luminary.set_rgb(red, green, blue, transition) if ATTR_COLOR_TEMP in kwargs: color_t = kwargs[ATTR_COLOR_TEMP] kelvin = int(color_temperature_mired_to_kelvin(color_t)) - _LOGGER.debug("turn_on requested set_temperature for light: " - "%s: %s", self._name, kelvin) self._luminary.set_temperature(kelvin, transition) if ATTR_EFFECT in kwargs: @@ -202,23 +184,16 @@ class Luminary(Light): self._luminary.set_rgb( random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255), transition) - _LOGGER.debug("turn_on requested random effect for light: " - "%s with transition %s", self._name, transition) self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGER.debug("Attempting to turn off light: %s", self._name) if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_luminance(0, transition) else: transition = 0 - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_onoff(0) self.schedule_update_ha_state() @@ -240,7 +215,8 @@ class OsramLightifyLight(Luminary): """Update status of a light.""" super().update() self._state = self._luminary.on() - self._rgb = self._luminary.rgb() + rgb = self._luminary.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = self._luminary.temp() if o_temp == 0: self._temperature = None @@ -270,7 +246,8 @@ class OsramLightifyGroup(Luminary): self._light_ids = self._luminary.lights() light = self._bridge.lights()[self._light_ids[0]] self._brightness = int(light.lum() * 2.55) - self._rgb = light.rgb() + rgb = light.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = light.temp() if o_temp == 0: self._temperature = None diff --git a/homeassistant/components/light/piglow.py b/homeassistant/components/light/piglow.py index 40798810c0e..755cf9dca66 100644 --- a/homeassistant/components/light/piglow.py +++ b/homeassistant/components/light/piglow.py @@ -11,15 +11,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['piglow==1.2.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'Piglow' @@ -50,7 +51,7 @@ class PiglowLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -63,9 +64,9 @@ class PiglowLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -93,15 +94,15 @@ class PiglowLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] - self._piglow.red(int(self._rgb_color[0] * percent_bright)) - self._piglow.green(int(self._rgb_color[1] * percent_bright)) - self._piglow.blue(int(self._rgb_color[2] * percent_bright)) - else: - self._piglow.all(self._brightness) + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._piglow.red(rgb[0]) + self._piglow.green(rgb[1]) + self._piglow.blue(rgb[2]) self._piglow.show() self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index 55b64bf8a74..9385c4bfb80 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['pwmled==1.2.1'] @@ -33,10 +34,10 @@ CONF_LED_TYPE_RGB = 'rgb' CONF_LED_TYPE_RGBW = 'rgbw' CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] -DEFAULT_COLOR = [255, 255, 255] +DEFAULT_COLOR = [0, 0] SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) -SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) +SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [ @@ -169,7 +170,7 @@ class PwmRgbLed(PwmSimpleLed): self._color = DEFAULT_COLOR @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -180,8 +181,8 @@ class PwmRgbLed(PwmSimpleLed): def turn_on(self, **kwargs): """Turn on a LED.""" - if ATTR_RGB_COLOR in kwargs: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -209,4 +210,5 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" from pwmled import Color - return Color(*tuple(color)) + rgb = color_util.color_hs_to_RGB(*color) + return Color(*tuple(rgb)) diff --git a/homeassistant/components/light/sensehat.py b/homeassistant/components/light/sensehat.py index 6c5467f8c6d..6ab2592cedf 100644 --- a/homeassistant/components/light/sensehat.py +++ b/homeassistant/components/light/sensehat.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['sense-hat==2.2.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'sensehat' @@ -49,7 +50,7 @@ class SenseHatLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -62,12 +63,9 @@ class SenseHatLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -93,14 +91,13 @@ class SenseHatLight(Light): """Instruct the light to turn on and set correct brightness & color.""" if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] - self._sensehat.clear(int(self._rgb_color[0] * percent_bright), - int(self._rgb_color[1] * percent_bright), - int(self._rgb_color[2] * percent_bright)) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._sensehat.clear(*rgb) self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/skybell.py b/homeassistant/components/light/skybell.py index 012190023fa..d32183f1468 100644 --- a/homeassistant/components/light/skybell.py +++ b/homeassistant/components/light/skybell.py @@ -8,10 +8,11 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.skybell import ( DOMAIN as SKYBELL_DOMAIN, SkybellDevice) +import homeassistant.util.color as color_util DEPENDENCIES = ['skybell'] @@ -54,8 +55,9 @@ class SkybellLight(SkybellDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if ATTR_RGB_COLOR in kwargs: - self._device.led_rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self._device.led_rgb = rgb elif ATTR_BRIGHTNESS in kwargs: self._device.led_intensity = _to_skybell_level( kwargs[ATTR_BRIGHTNESS]) @@ -77,11 +79,11 @@ class SkybellLight(SkybellDevice, Light): return _to_hass_level(self._device.led_intensity) @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" - return self._device.led_rgb + return color_util.color_RGB_to_hs(*self._device.led_rgb) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index c39748e4430..2079638f7f1 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['tikteck==0.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -57,7 +58,7 @@ class TikteckLight(Light): self._address = device['address'] self._password = device['password'] self._brightness = 255 - self._rgb = [255, 255, 255] + self._hs = [0, 0] self._state = False self.is_valid = True self._bulb = tikteck.tikteck( @@ -88,9 +89,9 @@ class TikteckLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs @property def supported_features(self): @@ -115,16 +116,17 @@ class TikteckLight(Light): """Turn the specified light on.""" self._state = True - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) - if rgb is not None: - self._rgb = rgb + if hs_color is not None: + self._hs = hs_color if brightness is not None: self._brightness = brightness - self.set_state(self._rgb[0], self._rgb[1], self._rgb[2], - self.brightness) + rgb = color_util.color_hs_to_RGB(*self._hs) + + self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index f87d624b83a..0bbec010282 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -5,23 +5,20 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.tplink/ """ import logging -import colorsys import time import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv 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 typing import Tuple - REQUIREMENTS = ['pyHS100==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -56,22 +53,6 @@ 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.""" @@ -83,7 +64,7 @@ class TPLinkSmartBulb(Light): self._available = True self._color_temp = None self._brightness = None - self._rgb = None + self._hs = None self._supported_features = 0 self._emeter_params = {} @@ -114,9 +95,10 @@ class TPLinkSmartBulb(Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) self.smartbulb.brightness = brightness_to_percentage(brightness) - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs.get(ATTR_RGB_COLOR) - self.smartbulb.hsv = rgb_to_hsv(rgb) + if ATTR_HS_COLOR in kwargs: + hue, sat = kwargs.get(ATTR_HS_COLOR) + hsv = (hue, sat, 100) + self.smartbulb.hsv = hsv def turn_off(self, **kwargs): """Turn the light off.""" @@ -133,9 +115,9 @@ class TPLinkSmartBulb(Light): return self._brightness @property - def rgb_color(self): - """Return the color in RGB.""" - return self._rgb + def hs_color(self): + """Return the color.""" + return self._hs @property def is_on(self): @@ -168,8 +150,9 @@ class TPLinkSmartBulb(Light): self._color_temp = kelvin_to_mired( self.smartbulb.color_temp) - if self._supported_features & SUPPORT_RGB_COLOR: - self._rgb = hsv_to_rgb(self.smartbulb.hsv) + if self._supported_features & SUPPORT_COLOR: + hue, sat, _ = self.smartbulb.hsv + self._hs = (hue, sat) if self.smartbulb.has_emeter: self._emeter_params[ATTR_CURRENT_POWER_W] = '{:.1f}'.format( @@ -203,4 +186,4 @@ class TPLinkSmartBulb(Light): if self.smartbulb.is_variable_color_temp: self._supported_features += SUPPORT_COLOR_TEMP if self.smartbulb.is_color: - self._supported_features += SUPPORT_RGB_COLOR + self._supported_features += SUPPORT_COLOR diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index bb2fa44c15c..1851579a172 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -10,9 +10,9 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ @@ -157,7 +157,7 @@ class TradfriLight(Light): self._light_control = None self._light_data = None self._name = None - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES self._temp_supported = False self._available = True @@ -237,9 +237,9 @@ class TradfriLight(Light): ) @property - def rgb_color(self): - """RGB color of the light.""" - return self._rgb_color + def hs_color(self): + """HS color of the light.""" + return self._hs_color @asyncio.coroutine def async_turn_off(self, **kwargs): @@ -252,12 +252,12 @@ class TradfriLight(Light): Instruct the light to turn on. After adding "self._light_data.hexcolor is not None" - for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. + for ATTR_HS_COLOR, this also supports Philips Hue bulbs. """ - if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: + if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) yield from self._api( - self._light.light_control.set_rgb_color( - *kwargs[ATTR_RGB_COLOR])) + self._light.light_control.set_rgb_color(*rgb)) elif ATTR_COLOR_TEMP in kwargs and \ self._light_data.hex_color is not None and \ @@ -309,17 +309,17 @@ class TradfriLight(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES if self._light.device_info.manufacturer == IKEA: if self._light_control.can_set_kelvin: self._features |= SUPPORT_COLOR_TEMP if self._light_control.can_set_color: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR else: if self._light_data.hex_color is not None: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR self._temp_supported = self._light.device_info.manufacturer \ in ALLOWED_TEMPERATURES @@ -328,7 +328,8 @@ class TradfriLight(Light): def _observe_update(self, tradfri_device): """Receive new state data for this light.""" self._refresh(tradfri_device) - self._rgb_color = color_util.rgb_hex_to_rgb_list( + rgb = color_util.rgb_hex_to_rgb_list( self._light_data.hex_color_inferred ) + self._hs_color = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 102ca814882..6b12e69341d 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/light.vera/ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class VeraLight(VeraDevice, Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" return self._color @@ -50,13 +51,14 @@ class VeraLight(VeraDevice, Light): def supported_features(self): """Flag supported features.""" if self._color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs and self._color: - self.vera_device.set_color(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs and self._color: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self.vera_device.set_color(rgb) elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS]) else: @@ -83,4 +85,5 @@ class VeraLight(VeraDevice, Light): # If it is dimmable, both functions exist. In case color # is not supported, it will return None self._brightness = self.vera_device.get_brightness() - self._color = self.vera_device.get_color() + rgb = self.vera_device.get_color() + self._color = color_util.color_RGB_to_hs(*rgb) if rgb else None diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 540c718b04d..d0575105235 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -9,12 +9,11 @@ import logging from datetime import timedelta import homeassistant.util as util -import homeassistant.util.color as color_util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) from homeassistant.loader import get_component +import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -23,8 +22,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) _LOGGER = logging.getLogger(__name__) -SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) +SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | + SUPPORT_TRANSITION) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -89,9 +88,10 @@ class WemoLight(Light): return self.device.state.get('level', 255) @property - def xy_color(self): - """Return the XY color values of this light.""" - return self.device.state.get('color_xy') + def hs_color(self): + """Return the hs color values of this light.""" + xy_color = self.device.state.get('color_xy') + return color_util.color_xy_to_hs(*xy_color) if xy_color else None @property def color_temp(self): @@ -112,17 +112,11 @@ class WemoLight(Light): """Turn the light on.""" transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - if ATTR_XY_COLOR in kwargs: - xycolor = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - xycolor = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2]) - else: - xycolor = None + hs_color = kwargs.get(ATTR_HS_COLOR) - if xycolor is not None: - self.device.set_color(xycolor, transition=transitiontime) + if hs_color is not None: + xy_color = color_util.color_hs_to_xy(*hs_color) + self.device.set_color(xy_color, transition=transitiontime) if ATTR_COLOR_TEMP in kwargs: colortemp = kwargs[ATTR_COLOR_TEMP] diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index e329fa04837..fd957f8f11d 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,8 +8,8 @@ import asyncio import colorsys from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.util import color as color_util from homeassistant.util.color import \ @@ -17,7 +17,7 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR +SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR def setup_platform(hass, config, add_devices, discovery_info=None): @@ -72,11 +72,11 @@ class WinkLight(WinkDevice, Light): return r_value, g_value, b_value @property - def xy_color(self): - """Define current bulb color in CIE 1931 (XY) color space.""" + def hs_color(self): + """Define current bulb color.""" if not self.wink.supports_xy_color(): return None - return self.wink.color_xy() + return color_util.color_xy_to_hs(*self.wink.color_xy()) @property def color_temp(self): @@ -94,21 +94,17 @@ class WinkLight(WinkDevice, Light): def turn_on(self, **kwargs): """Turn the switch on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - rgb_color = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - state_kwargs = { - } + state_kwargs = {} - if rgb_color: + if hs_color: if self.wink.supports_xy_color(): - xyb = color_util.color_RGB_to_xy(*rgb_color) - state_kwargs['color_xy'] = xyb[0], xyb[1] - state_kwargs['brightness'] = xyb[2] + xy_color = color_util.color_hs_to_xy(*hs_color) + state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - hsv = colorsys.rgb_to_hsv( - rgb_color[0], rgb_color[1], rgb_color[2]) - state_kwargs['color_hue_saturation'] = hsv[0], hsv[1] + state_kwargs['color_hue_saturation'] = hs_color if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index efe37d3d577..125e791829f 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -4,9 +4,10 @@ import struct import binascii from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, +from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): def __init__(self, device, name, xiaomi_hub): """Initialize the XiaomiGatewayLight.""" self._data_key = 'rgb' - self._rgb = (255, 255, 255) + self._hs = (0, 0) self._brightness = 180 XiaomiDevice.__init__(self, device, name, xiaomi_hub) @@ -64,7 +65,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): rgb = rgba[1:] self._brightness = int(255 * brightness / 100) - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._state = True return True @@ -74,24 +75,25 @@ class XiaomiGatewayLight(XiaomiDevice, Light): return self._brightness @property - def rgb_color(self): - """Return the RBG color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def supported_features(self): """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) - rgba = (self._brightness,) + self._rgb + rgb = color_util.color_hs_to_RGB(*self._hs) + rgba = (self._brightness,) + rgb rgbhex = binascii.hexlify(struct.pack('BBBB', *rgba)).decode("ASCII") rgbhex = int(rgbhex, 16) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index d9b7d6c76db..21a27c33203 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -37,25 +37,37 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ['philips.light.sread1', 'philips.light.ceiling', 'philips.light.zyceiling', - 'philips.light.bulb']), + 'philips.light.bulb', + 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 CCT_MAX = 100 -DELAYED_TURN_OFF_MAX_DEVIATION = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1 SUCCESS = ['ok'] ATTR_MODEL = 'model' ATTR_SCENE = 'scene' ATTR_DELAYED_TURN_OFF = 'delayed_turn_off' ATTR_TIME_PERIOD = 'time_period' +ATTR_NIGHT_LIGHT_MODE = 'night_light_mode' +ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' +ATTR_REMINDER = 'reminder' +ATTR_EYECARE_MODE = 'eyecare_mode' SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' +SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' +SERVICE_REMINDER_OFF = 'xiaomi_miio_reminder_off' +SERVICE_NIGHT_LIGHT_MODE_ON = 'xiaomi_miio_night_light_mode_on' +SERVICE_NIGHT_LIGHT_MODE_OFF = 'xiaomi_miio_night_light_mode_off' +SERVICE_EYECARE_MODE_ON = 'xiaomi_miio_eyecare_mode_on' +SERVICE_EYECARE_MODE_OFF = 'xiaomi_miio_eyecare_mode_off' XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -78,12 +90,18 @@ SERVICE_TO_METHOD = { SERVICE_SET_SCENE: { 'method': 'async_set_scene', 'schema': SERVICE_SCHEMA_SET_SCENE}, + SERVICE_REMINDER_ON: {'method': 'async_reminder_on'}, + SERVICE_REMINDER_OFF: {'method': 'async_reminder_off'}, + SERVICE_NIGHT_LIGHT_MODE_ON: {'method': 'async_night_light_mode_on'}, + SERVICE_NIGHT_LIGHT_MODE_OFF: {'method': 'async_night_light_mode_off'}, + SERVICE_EYECARE_MODE_ON: {'method': 'async_eyecare_mode_on'}, + SERVICE_EYECARE_MODE_OFF: {'method': 'async_eyecare_mode_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the light from config.""" from miio import Device, DeviceException if DATA_KEY not in hass.data: @@ -96,11 +114,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + devices = [] + unique_id = None + if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -111,27 +133,38 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if model == 'philips.light.sread1': from miio import PhilipsEyecare light = PhilipsEyecare(host, token) - device = XiaomiPhilipsEyecareLamp(name, light, model) + primary_device = XiaomiPhilipsEyecareLamp( + name, light, model, unique_id) + devices.append(primary_device) + hass.data[DATA_KEY][host] = primary_device + + secondary_device = XiaomiPhilipsEyecareLampAmbientLight( + name, light, model, unique_id) + devices.append(secondary_device) + # The ambient light doesn't expose additional services. + # A hass.data[DATA_KEY] entry isn't needed. elif model in ['philips.light.ceiling', 'philips.light.zyceiling']: from miio import Ceil light = Ceil(host, token) - device = XiaomiPhilipsCeilingLamp(name, light, model) - elif model == 'philips.light.bulb': + device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device + elif model in ['philips.light.bulb', 'philips.light.candle2']: from miio import PhilipsBulb light = PhilipsBulb(host, token) - device = XiaomiPhilipsLightBall(name, light, model) + device = XiaomiPhilipsBulb(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' - 'https://github.com/rytilahti/python-miio/issues ' + 'https://github.com/syssi/philipslight/issues ' 'and provide the following data: %s', model) return False - hass.data[DATA_KEY][host] = device - async_add_devices([device], update_before_add=True) + async_add_devices(devices, update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on Xiaomi Philips Lights.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() @@ -145,11 +178,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): update_tasks = [] for target_device in target_devices: - yield from getattr(target_device, method['method'])(**params) + if not hasattr(target_device, method['method']): + continue + await getattr(target_device, method['method'])(**params) update_tasks.append(target_device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for xiaomi_miio_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( @@ -158,23 +193,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema) -class XiaomiPhilipsGenericLight(Light): - """Representation of a Xiaomi Philips Light.""" +class XiaomiPhilipsAbstractLight(Light): + """Representation of a Abstract Xiaomi Philips Light.""" - def __init__(self, name, light, model): + def __init__(self, name, light, model, unique_id): """Initialize the light device.""" self._name = name + self._light = light self._model = model + self._unique_id = unique_id self._brightness = None - self._color_temp = None - self._light = light + self._available = False self._state = None self._state_attrs = { ATTR_MODEL: self._model, - ATTR_SCENE: None, - ATTR_DELAYED_TURN_OFF: None, } @property @@ -182,6 +216,11 @@ class XiaomiPhilipsGenericLight(Light): """Poll the light.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -190,7 +229,7 @@ class XiaomiPhilipsGenericLight(Light): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -212,12 +251,11 @@ class XiaomiPhilipsGenericLight(Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from light: %s", result) @@ -225,10 +263,10 @@ class XiaomiPhilipsGenericLight(Light): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -238,30 +276,57 @@ class XiaomiPhilipsGenericLight(Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) if result: self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - yield from self._try_command( + await self._try_command( "Turning the light off failed.", self._light.off) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): + """Representation of a Generic Xiaomi Philips Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_SCENE: None, + ATTR_DELAYED_TURN_OFF: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -276,45 +341,35 @@ class XiaomiPhilipsGenericLight(Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @asyncio.coroutine - def async_set_scene(self, scene: int = 1): + async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" - yield from self._try_command( + await self._try_command( "Setting a fixed scene failed.", self._light.set_scene, scene) - @asyncio.coroutine - def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delay off. The unit is different per device.""" - yield from self._try_command( - "Setting the delay off failed.", + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", self._light.delay_off, time_period.total_seconds()) - @staticmethod - def translate(value, left_min, left_max, right_min, right_max): - """Map a value from left span to right span.""" - left_span = left_max - left_min - right_span = right_max - right_min - value_scaled = float(value - left_min) / float(left_span) - return int(right_min + (value_scaled * right_span)) - @staticmethod def delayed_turn_off_timestamp(countdown: int, current: datetime, previous: datetime): """Update the turn off timestamp only if necessary.""" - if countdown > 0: + if countdown is not None and countdown > 0: new = current.replace(microsecond=0) + \ timedelta(seconds=countdown) if previous is None: return new - lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION) - upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION) + lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) + upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) diff = previous - new if lower < diff < upper: return previous @@ -324,8 +379,14 @@ class XiaomiPhilipsGenericLight(Light): return None -class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): - """Representation of a Xiaomi Philips Light Ball.""" +class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): + """Representation of a Xiaomi Philips Bulb.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._color_temp = None @property def color_temp(self): @@ -347,8 +408,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -367,7 +427,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): brightness, percent_brightness, color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness and color temperature failed: " "%s bri, %s cct", self._light.set_brightness_and_color_temperature, @@ -383,7 +443,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "%s mireds, %s%% cct", color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting color temperature failed: %s cct", self._light.set_color_temperature, percent_color_temp) @@ -398,7 +458,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) @@ -406,17 +466,17 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -435,13 +495,30 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + @staticmethod + def translate(value, left_min, left_max, right_min, right_max): + """Map a value from left span to right span.""" + left_span = left_max - left_min + right_span = right_max - right_min + value_scaled = float(value - left_min) / float(left_span) + return int(right_min + (value_scaled * right_span)) -class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): + +class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Ceiling Lamp.""" + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: None, + }) + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" @@ -452,8 +529,191 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): """Return the warmest color_temp that this light supports.""" return 370 + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) -class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight, Light): + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: + state.automatic_color_temperature, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - pass + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_REMINDER: None, + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_EYECARE_MODE: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_REMINDER: state.reminder, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_EYECARE_MODE: state.eyecare, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", + self._light.delay_off, round(time_period.total_seconds() / 60)) + + async def async_reminder_on(self): + """Enable the eye fatigue notification.""" + await self._try_command( + "Turning on the reminder failed.", + self._light.reminder_on) + + async def async_reminder_off(self): + """Disable the eye fatigue notification.""" + await self._try_command( + "Turning off the reminder failed.", + self._light.reminder_off) + + async def async_night_light_mode_on(self): + """Turn the smart night light mode on.""" + await self._try_command( + "Turning on the smart night light mode failed.", + self._light.smart_night_light_on) + + async def async_night_light_mode_off(self): + """Turn the smart night light mode off.""" + await self._try_command( + "Turning off the smart night light mode failed.", + self._light.smart_night_light_off) + + async def async_eyecare_mode_on(self): + """Turn the eyecare mode on.""" + await self._try_command( + "Turning on the eyecare mode failed.", + self._light.eyecare_on) + + async def async_eyecare_mode_off(self): + """Turn the eyecare mode off.""" + await self._try_command( + "Turning off the eyecare mode failed.", + self._light.eyecare_off) + + @staticmethod + def delayed_turn_off_timestamp(countdown: int, + current: datetime, + previous: datetime): + """Update the turn off timestamp only if necessary.""" + if countdown is not None and countdown > 0: + new = current.replace(second=0, microsecond=0) + \ + timedelta(minutes=countdown) + + if previous is None: + return new + + lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + diff = previous - new + if lower < diff < upper: + return previous + + return new + + return None + + +class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): + """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + name = '{} Ambient Light'.format(name) + if unique_id is not None: + unique_id = "{}-{}".format(unique_id, 'ambient') + super().__init__(name, light, model, unique_id) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + percent_brightness = ceil(100 * brightness / 255.0) + + _LOGGER.debug( + "Setting brightness of the ambient light: %s %s%%", + brightness, percent_brightness) + + result = await self._try_command( + "Setting brightness of the ambient failed: %s", + self._light.set_ambient_brightness, percent_brightness) + + if result: + self._brightness = brightness + else: + await self._try_command( + "Turning the ambient light on failed.", self._light.ambient_on) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._try_command( + "Turning the ambient light off failed.", self._light.ambient_off) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.eyecare + self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index ca10d246ce8..585db950efc 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -5,26 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.yeelight/ """ import logging -import colorsys -from typing import Tuple import voluptuous as vol from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired, - color_temperature_to_rgb, - color_RGB_to_xy, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired as kelvin_to_mired) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, - ATTR_FLASH, ATTR_XY_COLOR, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_XY_COLOR, - SUPPORT_TRANSITION, - SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT, - Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, + ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, + SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelight==0.4.0'] @@ -53,8 +47,7 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH) SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT | - SUPPORT_RGB_COLOR | - SUPPORT_XY_COLOR | + SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) @@ -98,14 +91,6 @@ 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): @@ -157,8 +142,7 @@ class YeelightLight(Light): self._brightness = None self._color_temp = None self._is_on = None - self._rgb = None - self._xy = None + self._hs = None @property def available(self) -> bool: @@ -209,38 +193,32 @@ class YeelightLight(Light): return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN) return kelvin_to_mired(YEELIGHT_MIN_KELVIN) - def _get_rgb_from_properties(self): + def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) color_mode = self._properties.get('color_mode', None) if not rgb or not color_mode: - return rgb + return None color_mode = int(color_mode) if color_mode == 2: # color temperature temp_in_k = mired_to_kelvin(self._color_temp) - return color_temperature_to_rgb(temp_in_k) + return color_util.color_temperature_to_hs(temp_in_k) if color_mode == 3: # hsv 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)) + return (hue / 360 * 65536, sat / 100 * 255) rgb = int(rgb) blue = rgb & 0xff green = (rgb >> 8) & 0xff red = (rgb >> 16) & 0xff - return red, green, blue + return color_util.color_RGB_to_hs(red, green, blue) @property - def rgb_color(self) -> tuple: + def hs_color(self) -> tuple: """Return the color property.""" - return self._rgb - - @property - def xy_color(self) -> tuple: - """Return the XY color value.""" - return self._xy + return self._hs @property def _properties(self) -> dict: @@ -288,13 +266,7 @@ class YeelightLight(Light): if temp_in_k: self._color_temp = kelvin_to_mired(int(temp_in_k)) - self._rgb = self._get_rgb_from_properties() - - if self._rgb: - xyb = color_RGB_to_xy(*self._rgb) - self._xy = (xyb[0], xyb[1]) - else: - self._xy = None + self._hs = self._get_hs_from_properties() self._available = True except yeelight.BulbException as ex: @@ -313,7 +285,7 @@ class YeelightLight(Light): @_cmd def set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" - if rgb and self.supported_features & SUPPORT_RGB_COLOR: + if rgb and self.supported_features & SUPPORT_COLOR: _LOGGER.debug("Setting RGB: %s", rgb) self._bulb.set_rgb(rgb[0], rgb[1], rgb[2], duration=duration) @@ -349,7 +321,7 @@ class YeelightLight(Light): count = 1 duration = transition * 2 - red, green, blue = self.rgb_color + red, green, blue = color_util.color_hs_to_RGB(*self._hs) transitions = list() transitions.append( @@ -419,10 +391,10 @@ class YeelightLight(Light): import yeelight brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None flash = kwargs.get(ATTR_FLASH) effect = kwargs.get(ATTR_EFFECT) - xy_color = kwargs.get(ATTR_XY_COLOR) duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config @@ -440,9 +412,6 @@ class YeelightLight(Light): except yeelight.BulbException as ex: _LOGGER.error("Unable to turn on music mode," "consider disabling it: %s", ex) - if xy_color and brightness: - rgb = color_xy_brightness_to_RGB(xy_color[0], xy_color[1], - brightness) try: # values checked for none in methods diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index 5f48e3a0a71..88f86063c13 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS, + Light, ATTR_HS_COLOR, SUPPORT_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) from homeassistant.const import CONF_HOST +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelightsunflower==0.0.8'] _LOGGER = logging.getLogger(__name__) -SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string @@ -48,7 +49,7 @@ class SunflowerBulb(Light): self._available = light.available self._brightness = light.brightness self._is_on = light.is_on - self._rgb_color = light.rgb_color + self._hs_color = light.rgb_color @property def name(self): @@ -71,9 +72,9 @@ class SunflowerBulb(Light): return int(self._brightness / 100 * 255) @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -86,12 +87,12 @@ class SunflowerBulb(Light): if not kwargs: self._light.turn_on() else: - if ATTR_RGB_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) self._light.set_all(rgb[0], rgb[1], rgb[2], bright) - elif ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + elif ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._light.set_rgb_color(rgb[0], rgb[1], rgb[2]) elif ATTR_BRIGHTNESS in kwargs: bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -107,4 +108,4 @@ class SunflowerBulb(Light): self._available = self._light.available self._brightness = self._light.brightness self._is_on = self._light.is_on - self._rgb_color = self._light.rgb_color + self._hs_color = color_util.color_RGB_to_hs(*self._light.rgb_color) diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index 7071c8c43bb..3c77f2d8449 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, - SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['zengge==0.2'] _LOGGER = logging.getLogger(__name__) -SUPPORT_ZENGGE_LED = (SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) +SUPPORT_ZENGGE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -56,7 +57,8 @@ class ZenggeLight(Light): self.is_valid = True self._bulb = zengge.zengge(self._address) self._white = 0 - self._rgb = (0, 0, 0) + self._brightness = 0 + self._hs_color = (0, 0) self._state = False if self._bulb.connect() is False: self.is_valid = False @@ -80,9 +82,14 @@ class ZenggeLight(Light): return self._state @property - def rgb_color(self): + def brightness(self): + """Return the brightness property.""" + return self._brightness + + @property + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs_color @property def white_value(self): @@ -117,21 +124,29 @@ class ZenggeLight(Light): self._state = True self._bulb.on() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) white = kwargs.get(ATTR_WHITE_VALUE) + brightness = kwargs.get(ATTR_BRIGHTNESS) if white is not None: self._white = white - self._rgb = (0, 0, 0) + self._hs_color = (0, 0) - if rgb is not None: + if hs_color is not None: self._white = 0 - self._rgb = rgb + self._hs_color = hs_color + + if brightness is not None: + self._white = 0 + self._brightness = brightness if self._white != 0: self.set_white(self._white) else: - self.set_rgb(self._rgb[0], self._rgb[1], self._rgb[2]) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], + self._brightness / 255 * 100) + self.set_rgb(*rgb) def turn_off(self, **kwargs): """Turn the specified light off.""" @@ -140,6 +155,9 @@ class ZenggeLight(Light): def update(self): """Synchronise internal state with the actual light state.""" - self._rgb = self._bulb.get_colour() + rgb = self._bulb.get_colour() + hsv = color_util.color_RGB_to_hsv(*rgb) + self._hs_color = hsv[:2] + self._brightness = hsv[2] self._white = self._bulb.get_white() self._state = self._bulb.get_on() diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index f50b3d7689b..8eb1b3dc9b6 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -4,12 +4,10 @@ Lights on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/light.zha/ """ -import asyncio import logging - from homeassistant.components import light, zha -from homeassistant.util.color import color_RGB_to_xy from homeassistant.const import STATE_UNKNOWN +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -23,8 +21,8 @@ CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Zigbee Home Automation lights.""" discovery_info = zha.get_discovery_info(hass, discovery_info) if discovery_info is None: @@ -32,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): endpoint = discovery_info['endpoint'] if hasattr(endpoint, 'light_color'): - caps = yield from zha.safe_read( + caps = await zha.safe_read( endpoint.light_color, ['color_capabilities']) discovery_info['color_capabilities'] = caps.get('color_capabilities') if discovery_info['color_capabilities'] is None: @@ -40,7 +38,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # attribute. In this version XY support is mandatory, but we need # to probe to determine if the device supports color temperature. discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY - result = yield from zha.safe_read( + result = await zha.safe_read( endpoint.light_color, ['color_temperature']) if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE: discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP @@ -58,7 +56,7 @@ class Light(zha.Entity, light.Light): super().__init__(**kwargs) self._supported_features = 0 self._color_temp = None - self._xy_color = None + self._hs_color = None self._brightness = None import zigpy.zcl.clusters as zcl_clusters @@ -72,9 +70,8 @@ class Light(zha.Entity, light.Light): self._supported_features |= light.SUPPORT_COLOR_TEMP if color_capabilities & CAPABILITIES_COLOR_XY: - self._supported_features |= light.SUPPORT_XY_COLOR - self._supported_features |= light.SUPPORT_RGB_COLOR - self._xy_color = (1.0, 1.0) + self._supported_features |= light.SUPPORT_COLOR + self._hs_color = (0, 0) @property def is_on(self) -> bool: @@ -83,28 +80,22 @@ class Light(zha.Entity, light.Light): return False return bool(self._state) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) duration = duration * 10 # tenths of s if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] - yield from self._endpoint.light_color.move_to_color_temp( + await self._endpoint.light_color.move_to_color_temp( temperature, duration) self._color_temp = temperature - if light.ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[light.ATTR_XY_COLOR] - elif light.ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[light.ATTR_RGB_COLOR])) - self._xy_color = (xyb[0], xyb[1]) - self._brightness = xyb[2] - if light.ATTR_XY_COLOR in kwargs or light.ATTR_RGB_COLOR in kwargs: - yield from self._endpoint.light_color.move_to_color( - int(self._xy_color[0] * 65535), - int(self._xy_color[1] * 65535), + if light.ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[light.ATTR_HS_COLOR] + xy_color = color_util.color_hs_to_xy(*self._hs_color) + await self._endpoint.light_color.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), duration, ) @@ -113,22 +104,32 @@ class Light(zha.Entity, light.Light): light.ATTR_BRIGHTNESS, self._brightness or 255) self._brightness = brightness # Move to level with on/off: - yield from self._endpoint.level.move_to_level_with_on_off( + await self._endpoint.level.move_to_level_with_on_off( brightness, duration ) self._state = 1 self.async_schedule_update_ha_state() return + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light on: %s", ex) + return - yield from self._endpoint.on_off.on() self._state = 1 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" - yield from self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light off: %s", ex) + return + self._state = 0 self.async_schedule_update_ha_state() @@ -138,9 +139,9 @@ class Light(zha.Entity, light.Light): return self._brightness @property - def xy_color(self): - """Return the XY color value [float, float].""" - return self._xy_color + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs_color @property def color_temp(self): @@ -152,28 +153,28 @@ class Light(zha.Entity, light.Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" - result = yield from zha.safe_read(self._endpoint.on_off, ['on_off']) + result = await zha.safe_read(self._endpoint.on_off, ['on_off']) self._state = result.get('on_off', self._state) if self._supported_features & light.SUPPORT_BRIGHTNESS: - result = yield from zha.safe_read(self._endpoint.level, - ['current_level']) + result = await zha.safe_read(self._endpoint.level, + ['current_level']) self._brightness = result.get('current_level', self._brightness) if self._supported_features & light.SUPPORT_COLOR_TEMP: - result = yield from zha.safe_read(self._endpoint.light_color, - ['color_temperature']) + result = await zha.safe_read(self._endpoint.light_color, + ['color_temperature']) self._color_temp = result.get('color_temperature', self._color_temp) - if self._supported_features & light.SUPPORT_XY_COLOR: - result = yield from zha.safe_read(self._endpoint.light_color, - ['current_x', 'current_y']) + if self._supported_features & light.SUPPORT_COLOR: + result = await zha.safe_read(self._endpoint.light_color, + ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - self._xy_color = (result['current_x'], result['current_y']) + xy_color = (result['current_x'], result['current_y']) + self._hs_color = color_util.color_xy_to_hs(*xy_color) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 64c6530dd2b..286ce73f1ed 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -9,14 +9,14 @@ import logging # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ - ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light +from homeassistant.components.light import ( + ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) from homeassistant.components import zwave from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import color_temperature_mired_to_kelvin, \ - color_temperature_to_rgb, color_rgb_to_rgbw, color_rgbw_to_rgb +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -65,10 +65,11 @@ def brightness_state(value): return 0, STATE_OFF -def ct_to_rgb(temp): - """Convert color temperature (mireds) to RGB.""" +def ct_to_hs(temp): + """Convert color temperature (mireds) to hs.""" colorlist = list( - color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + color_util.color_temperature_to_hs( + color_util.color_temperature_mired_to_kelvin(temp))) return [int(val) for val in colorlist] @@ -209,8 +210,9 @@ class ZwaveColorLight(ZwaveDimmer): def __init__(self, values, refresh, delay): """Initialize the light.""" self._color_channels = None - self._rgb = None + self._hs = None self._ct = None + self._white = None super().__init__(values, refresh, delay) @@ -218,9 +220,12 @@ class ZwaveColorLight(ZwaveDimmer): """Call when a new value is added to this entity.""" super().value_added() - self._supported_features |= SUPPORT_RGB_COLOR + self._supported_features |= SUPPORT_COLOR if self._zw098: self._supported_features |= SUPPORT_COLOR_TEMP + elif self._color_channels is not None and self._color_channels & ( + COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE): + self._supported_features |= SUPPORT_WHITE_VALUE def update_properties(self): """Update internal properties based on zwave values.""" @@ -238,10 +243,11 @@ class ZwaveColorLight(ZwaveDimmer): data = self.values.color.data # RGB is always present in the openzwave color data string. - self._rgb = [ + rgb = [ int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] + self._hs = color_util.color_RGB_to_hs(*rgb) # Parse remaining color channels. Openzwave appends white channels # that are present. @@ -267,30 +273,35 @@ class ZwaveColorLight(ZwaveDimmer): if self._zw098: if warm_white > 0: self._ct = TEMP_WARM_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) elif cold_white > 0: self._ct = TEMP_COLD_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) else: # RGB color is being used. Just report midpoint. self._ct = TEMP_MID_HASS elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white)) + self._white = warm_white elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white)) + self._white = cold_white # If no rgb channels supported, report None. if not (self._color_channels & COLOR_CHANNEL_RED or self._color_channels & COLOR_CHANNEL_GREEN or self._color_channels & COLOR_CHANNEL_BLUE): - self._rgb = None + self._hs = None @property - def rgb_color(self): - """Return the rgb color.""" - return self._rgb + def hs_color(self): + """Return the hs color.""" + return self._hs + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white @property def color_temp(self): @@ -301,6 +312,9 @@ class ZwaveColorLight(ZwaveDimmer): """Turn the device on.""" rgbw = None + if ATTR_WHITE_VALUE in kwargs: + self._white = kwargs[ATTR_WHITE_VALUE] + if ATTR_COLOR_TEMP in kwargs: # Color temperature. With the AEOTEC ZW098 bulb, only two color # temperatures are supported. The warm and cold channel values @@ -313,19 +327,16 @@ class ZwaveColorLight(ZwaveDimmer): self._ct = TEMP_COLD_HASS rgbw = '#00000000ff' - elif ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] - if (not self._zw098 and ( - self._color_channels & COLOR_CHANNEL_WARM_WHITE or - self._color_channels & COLOR_CHANNEL_COLD_WHITE)): - rgbw = '#' - for colorval in color_rgb_to_rgbw(*self._rgb): - rgbw += format(colorval, '02x') - rgbw += '00' + elif ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] + + if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: + rgbw = '#' + for colorval in color_util.color_hs_to_RGB(*self._hs): + rgbw += format(colorval, '02x') + if self._white is not None: + rgbw += format(self._white, '02x') + '00' else: - rgbw = '#' - for colorval in self._rgb: - rgbw += format(colorval, '02x') rgbw += '0000' if rgbw and self.values.color: diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d03bbebd696..b3e4ac8f0ff 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) + STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_CODE): cv.string, }) +# Bitfield of features supported by the lock entity +SUPPORT_OPEN = 1 + _LOGGER = logging.getLogger(__name__) PROP_TO_ATTR = { @@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" @@ -97,6 +112,8 @@ def async_setup(hass, config): for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) + elif service.service == SERVICE_OPEN: + yield from entity.async_open(code=code) else: yield from entity.async_unlock(code=code) @@ -113,6 +130,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_OPEN, async_handle_lock_service, + schema=LOCK_SERVICE_SCHEMA) return True @@ -158,6 +178,17 @@ class LockDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + def open(self, **kwargs): + """Open the door latch.""" + raise NotImplementedError() + + def async_open(self, **kwargs): + """Open the door latch. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py new file mode 100644 index 00000000000..c500e02b2f7 --- /dev/null +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -0,0 +1,109 @@ +""" +Support for BMW cars with BMW ConnectedDrive. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/lock.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the BMW Connected Drive lock.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, 'lock', 'BMW lock') + devices.append(device) + add_devices(devices, True) + + +class BMWLock(LockDevice): + """Representation of a BMW vehicle lock.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name): + """Initialize the lock.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name + self._state = None + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the lock.""" + vehicle_state = self._vehicle.state + return { + 'car': self._vehicle.modelName, + 'door_lock_state': vehicle_state.door_lock_state.value + } + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + def lock(self, **kwargs): + """Lock the car.""" + _LOGGER.debug("%s: locking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_LOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_lock() + + def unlock(self, **kwargs): + """Unlock the car.""" + _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_unlock() + + def update(self): + """Update state of the lock.""" + _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName, + self._attribute) + vehicle_state = self._vehicle.state + + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value + in ('LOCKED', 'SECURED') else STATE_UNLOCKED) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index aca25e7e16d..d561dd333ab 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -4,7 +4,7 @@ Demo lock platform that has two fake locks. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) @@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED) + DemoLock('Kitchen Door', STATE_UNLOCKED), + DemoLock('Openable Lock', STATE_LOCKED, True) ]) class DemoLock(LockDevice): """Representation of a Demo lock.""" - def __init__(self, name, state): + def __init__(self, name, state, openable=False): """Initialize the lock.""" self._name = name self._state = state + self._openable = openable @property def should_poll(self): @@ -49,3 +51,14 @@ class DemoLock(LockDevice): """Unlock the device.""" self._state = STATE_UNLOCKED self.schedule_update_ha_state() + + def open(self, **kwargs): + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/lock/homematic.py new file mode 100644 index 00000000000..0d70849e37e --- /dev/null +++ b/homeassistant/components/lock/homematic.py @@ -0,0 +1,58 @@ +""" +Support for Homematic lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homematic/ +""" +import logging +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Homematic lock platform.""" + if discovery_info is None: + return + + devices = [] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + devices.append(HMLock(conf)) + + add_devices(devices) + + +class HMLock(HMDevice, LockDevice): + """Representation of a Homematic lock aka KeyMatic.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return not bool(self._hm_get_state()) + + def lock(self, **kwargs): + """Lock the lock.""" + self._hmdevice.lock() + + def unlock(self, **kwargs): + """Unlock the lock.""" + self._hmdevice.unlock() + + def open(self, **kwargs): + """Open the door latch.""" + self._hmdevice.open() + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index e73e35a9900..d8af22cd5c3 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -44,6 +44,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT lock.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index d0b944793c4..1c3e8ed1f19 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -139,9 +139,12 @@ class LogbookView(HomeAssistantView): end_day = start_day + timedelta(days=1) hass = request.app['hass'] - events = yield from hass.async_add_job( - _get_events, hass, self.config, start_day, end_day) - response = yield from hass.async_add_job(self.json, events) + def json_events(): + """Fetch events and generate JSON.""" + return self.json(list( + _get_events(hass, self.config, start_day, end_day))) + + response = yield from hass.async_add_job(json_events) return response diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 265784be74d..e10a713995b 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.02.11'] +REQUIREMENTS = ['youtube_dl==2018.03.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index a07e577c969..1b6310d4cab 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Bluesound platforms.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] @@ -149,8 +149,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, async_add_devices, host.get(CONF_HOST), host.get(CONF_PORT), host.get(CONF_NAME)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to method of Bluesound devices.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -166,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): target_players = hass.data[DATA_BLUESOUND] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -211,13 +210,12 @@ class BluesoundPlayer(MediaPlayerDevice): except ValueError: return -1 - @asyncio.coroutine - def force_update_sync_status( + async def force_update_sync_status( self, on_updated_cb=None, raise_timeout=False): """Update the internal status.""" resp = None try: - resp = yield from self.send_bluesound_command( + resp = await self.send_bluesound_command( 'SyncStatus', raise_timeout, raise_timeout) except Exception: raise @@ -254,16 +252,15 @@ class BluesoundPlayer(MediaPlayerDevice): on_updated_cb() return True - @asyncio.coroutine - def _start_poll_command(self): + async def _start_poll_command(self): """Loop which polls the status of the player.""" try: while True: - yield from self.async_update_status() + await self.async_update_status() except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self._name) - yield from asyncio.sleep( + await asyncio.sleep( NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) self.start_polling() @@ -282,15 +279,14 @@ class BluesoundPlayer(MediaPlayerDevice): """Stop the polling task.""" self._polling_task.cancel() - @asyncio.coroutine - def async_init(self): + async def async_init(self, triggered=None): """Initialize the player async.""" try: if self._retry_remove is not None: self._retry_remove() self._retry_remove = None - yield from self.force_update_sync_status( + await self.force_update_sync_status( self._init_callback, True) except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self.host) @@ -301,20 +297,18 @@ class BluesoundPlayer(MediaPlayerDevice): self.host) raise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update internal status of the entity.""" if not self._is_online: return - yield from self.async_update_sync_status() - yield from self.async_update_presets() - yield from self.async_update_captures() - yield from self.async_update_services() + await self.async_update_sync_status() + await self.async_update_presets() + await self.async_update_captures() + await self.async_update_services() - @asyncio.coroutine - def send_bluesound_command(self, method, raise_timeout=False, - allow_offline=False): + async def send_bluesound_command(self, method, raise_timeout=False, + allow_offline=False): """Send command to the player.""" import xmltodict @@ -330,10 +324,10 @@ class BluesoundPlayer(MediaPlayerDevice): try: websession = async_get_clientsession(self._hass) with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - result = yield from response.text() + result = await response.text() if len(result) < 1: data = None else: @@ -352,8 +346,7 @@ class BluesoundPlayer(MediaPlayerDevice): return data - @asyncio.coroutine - def async_update_status(self): + async def async_update_status(self): """Use the poll session to always get the status of the player.""" import xmltodict response = None @@ -372,7 +365,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: with async_timeout.timeout(125, loop=self._hass.loop): - response = yield from self._polling_session.get( + response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE}) @@ -380,7 +373,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.error("Error %s on %s. Trying one more time.", response.status, url) else: - result = yield from response.text() + result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() self._status = xmltodict.parse(result)['status'].copy() @@ -392,8 +385,8 @@ class BluesoundPlayer(MediaPlayerDevice): self._group_name = group_name # the sleep is needed to make sure that the # devices is synced - yield from asyncio.sleep(1, loop=self._hass.loop) - yield from self.async_trigger_sync_on_all() + await asyncio.sleep(1, loop=self._hass.loop) + await self.async_trigger_sync_on_all() elif self.is_grouped: # when player is grouped we need to fetch volume from # sync_status. We will force an update if the player is @@ -402,7 +395,7 @@ class BluesoundPlayer(MediaPlayerDevice): # the device is playing. This would solve alot of # problems. This change will be done when the # communication is moved to a separate library - yield from self.force_update_sync_status() + await self.force_update_sync_status() self.async_schedule_update_ha_state() @@ -415,13 +408,12 @@ class BluesoundPlayer(MediaPlayerDevice): self._name) raise - @asyncio.coroutine - def async_trigger_sync_on_all(self): + async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" _LOGGER.debug("Trigger sync status on all devices") for player in self._hass.data[DATA_BLUESOUND]: - yield from player.force_update_sync_status() + await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) async def async_update_sync_status(self, on_updated_cb=None, @@ -788,8 +780,7 @@ class BluesoundPlayer(MediaPlayerDevice): """Return true if shuffle is active.""" return True if self._status.get('shuffle', '0') == '1' else False - @asyncio.coroutine - def async_join(self, master): + async def async_join(self, master): """Join the player to a group.""" master_device = [device for device in self.hass.data[DATA_BLUESOUND] if device.entity_id == master] @@ -798,37 +789,33 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.debug("Trying to join player: %s to master: %s", self.host, master_device[0].host) - yield from master_device[0].async_add_slave(self) + await master_device[0].async_add_slave(self) else: _LOGGER.error("Master not found %s", master_device) - @asyncio.coroutine - def async_unjoin(self): + async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: return _LOGGER.debug("Trying to unjoin player: %s", self.host) - yield from self._master.async_remove_slave(self) + await self._master.async_remove_slave(self) - @asyncio.coroutine - def async_add_slave(self, slave_device): + async def async_add_slave(self, slave_device): """Add slave to master.""" return self.send_bluesound_command('/AddSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_remove_slave(self, slave_device): + async def async_remove_slave(self, slave_device): """Remove slave to master.""" return self.send_bluesound_command('/RemoveSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_increase_timer(self): + async def async_increase_timer(self): """Increase sleep time on player.""" - sleep_time = yield from self.send_bluesound_command('/Sleep') + sleep_time = await self.send_bluesound_command('/Sleep') if sleep_time is None: _LOGGER.error('Error while increasing sleep time on player: %s', self.host) @@ -836,21 +823,18 @@ class BluesoundPlayer(MediaPlayerDevice): return int(sleep_time.get('sleep', '0')) - @asyncio.coroutine - def async_clear_timer(self): + async def async_clear_timer(self): """Clear sleep timer on player.""" sleep = 1 while sleep > 0: - sleep = yield from self.async_increase_timer() + sleep = await self.async_increase_timer() - @asyncio.coroutine - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" return self.send_bluesound_command('/Shuffle?state={}' .format('1' if shuffle else '0')) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Select input source.""" if self.is_grouped and not self.is_master: return @@ -874,16 +858,14 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Clear') - @asyncio.coroutine - def async_media_next_track(self): + async def async_media_next_track(self): """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return @@ -897,8 +879,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return @@ -912,40 +893,35 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play') - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Send stop command.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_seek(self, position): + async def async_media_seek(self, position): """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play?seek=' + str(float(position))) - @asyncio.coroutine - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. @@ -961,24 +937,21 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Volume up the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)+1)/100) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)-1)/100) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" if volume < 0: volume = 0 @@ -987,8 +960,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command( 'Volume?level=' + str(float(volume) * 100)) - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command to media player.""" if mute: volume = self.volume_level diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 579f9b62864..91b8d362c43 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,8 +7,10 @@ https://home-assistant.io/components/media_player.cast/ # pylint: disable=import-error import logging import threading +from typing import Optional, Tuple import voluptuous as vol +import attr from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -22,11 +24,11 @@ from homeassistant.components.media_player import ( SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==2.0.0'] +REQUIREMENTS = ['pychromecast==2.1.0'] _LOGGER = logging.getLogger(__name__) @@ -39,23 +41,103 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY +# Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' -# UUID -> CastDevice mapping; cast devices without UUID are not stored +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' -# Stores every discovered (host, port, uuid) -KNOWN_CHROMECASTS_KEY = 'cast_all_chromecasts' +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# Chromecast or receive it through configuration SIGNAL_CAST_DISCOVERED = 'cast_discovered' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC): [cv.string], + vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, + [cv.string]) }) +@attr.s(slots=True, frozen=True) +class ChromecastInfo(object): + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + host = attr.ib(type=str) + port = attr.ib(type=int) + uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), + default=None) # always convert UUID to string if not None + model_name = attr.ib(type=str, default='') # needed for cast type + friendly_name = attr.ib(type=Optional[str], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + return all(attr.astuple(self)) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + +def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: + """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" + if info.is_information_complete or info.is_audio_group: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return info + + # Fill out missing information via HTTP dial. + from pychromecast import dial + + http_device_status = dial.get_device_status(info.host) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return info + + return ChromecastInfo( + host=info.host, port=info.port, + uuid=(info.uuid or http_device_status.uuid), + friendly_name=(info.friendly_name or http_device_status.friendly_name), + model_name=(info.model_name or http_device_status.model_name) + ) + + +def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + return + + # Either discovered completely new chromecast or a "moved" one. + info = _fill_out_missing_chromecast_info(info) + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] + if info.uuid == x.uuid) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) + + def _setup_internal_discovery(hass: HomeAssistantType) -> None: """Set up the pychromecast internal discovery.""" - hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock()) + if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): # Internal discovery is already running return @@ -65,30 +147,14 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" mdns = listener.services[name] - ip_address, port, uuid, _, _ = mdns - key = (ip_address, port, uuid) - - if key in hass.data[KNOWN_CHROMECASTS_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", mdns) - return - - _LOGGER.debug("Discovered new chromecast %s", mdns) - try: - # pylint: disable=protected-access - chromecast = pychromecast._get_chromecast_from_host( - mdns, blocking=True) - except pychromecast.ChromecastConnectionError: - _LOGGER.debug("Can't set up cast with mDNS info %s. " - "Assuming it's not a Chromecast", mdns) - return - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, chromecast) + _discover_chromecast(hass, ChromecastInfo(*mdns)) _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery(internal_callback) def stop_discovery(event): """Stop discovery of new chromecasts.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") pychromecast.stop_discovery(browser) hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() @@ -96,40 +162,26 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: @callback -def _async_create_cast_device(hass, chromecast): +def _async_create_cast_device(hass: HomeAssistantType, + info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. - Returns None if the cast device has already been added. Additionally, - automatically updates existing chromecast entities. + Returns None if the cast device has already been added. """ - if chromecast.uuid is None: + if info.uuid is None: # Found a cast without UUID, we don't store it because we won't be able # to update it anyway. - return CastDevice(chromecast) + return CastDevice(info) # Found a cast with UUID added_casts = hass.data[ADDED_CAST_DEVICES_KEY] - old_cast_device = added_casts.get(chromecast.uuid) - if old_cast_device is None: - # -> New cast device - cast_device = CastDevice(chromecast) - added_casts[chromecast.uuid] = cast_device - return cast_device - - old_key = (old_cast_device.cast.host, - old_cast_device.cast.port, - old_cast_device.cast.uuid) - new_key = (chromecast.host, chromecast.port, chromecast.uuid) - - if old_key == new_key: - # Re-discovered with same data, ignore + if info.uuid in added_casts: + # Already added this one, the entity will take care of moved hosts + # itself return None - - # -> Cast device changed host - # Remove old pychromecast.Chromecast from global list, because it isn't - # valid anymore - old_cast_device.async_set_chromecast(chromecast) - return None + # -> New cast device + added_casts.add(info.uuid) + return CastDevice(info) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -139,98 +191,308 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) - hass.data.setdefault(ADDED_CAST_DEVICES_KEY, {}) - hass.data.setdefault(KNOWN_CHROMECASTS_KEY, {}) + hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) - # None -> use discovery; (host, port) -> manually specify chromecast. - want_host = None - if discovery_info: - want_host = (discovery_info.get('host'), discovery_info.get('port')) + info = None + if discovery_info is not None: + info = ChromecastInfo(host=discovery_info['host'], + port=discovery_info['port']) elif CONF_HOST in config: - want_host = (config.get(CONF_HOST), DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], + port=DEFAULT_PORT) - enable_discovery = False - if want_host is None: - # We were explicitly told to enable pychromecast discovery. - enable_discovery = True - elif want_host[1] != DEFAULT_PORT: - # We're trying to add a group, so we have to use pychromecast's - # discovery to get the correct friendly name. - enable_discovery = True + @callback + def async_cast_discovered(discover: ChromecastInfo) -> None: + """Callback for when a new chromecast is discovered.""" + if info is not None and info.host_port != discover.host_port: + # Not our requested cast device. + return - if enable_discovery: - @callback - def async_cast_discovered(chromecast): - """Callback for when a new chromecast is discovered.""" - if want_host is not None and \ - (chromecast.host, chromecast.port) != want_host: - return # for groups, only add requested device - cast_device = _async_create_cast_device(hass, chromecast) + cast_device = _async_create_cast_device(hass, discover) + if cast_device is not None: + async_add_devices([cast_device]) - if cast_device is not None: - async_add_devices([cast_device]) - - async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) - # Re-play the callback for all past chromecasts, store the objects in - # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECASTS_KEY].values()): - async_cast_discovered(chromecast) + async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + # Re-play the callback for all past chromecasts, store the objects in + # a list to avoid concurrent modification resulting in exception. + for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + async_cast_discovered(chromecast) + if info is None or info.is_audio_group: + # If we were a) explicitly told to enable discovery or + # b) have an audio group cast device, we need internal discovery. hass.async_add_job(_setup_internal_discovery, hass) else: - # Manually add a "normal" Chromecast, we can do that without discovery. - try: - chromecast = await hass.async_add_job( - pychromecast.Chromecast, *want_host) - except pychromecast.ChromecastConnectionError as err: - _LOGGER.warning("Can't set up chromecast on %s: %s", - want_host[0], err) + info = await hass.async_add_job(_fill_out_missing_chromecast_info, + info) + if info.friendly_name is None: + # HTTP dial failed, so we won't be able to connect. raise PlatformNotReady - key = (chromecast.host, chromecast.port, chromecast.uuid) - cast_device = _async_create_cast_device(hass, chromecast) - if cast_device is not None: - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - async_add_devices([cast_device]) + hass.async_add_job(_discover_chromecast, hass, info) + + +class CastStatusListener(object): + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast): + """Initialize the status listener.""" + self._cast_device = cast_device + self._valid = True + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener( + self) + chromecast.register_connection_listener(self) + + def new_cast_status(self, cast_status): + """Called when a new CastStatus is received.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Called when a new MediaStatus is received.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Called when a new ConnectionStatus is received.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._valid = False class CastDevice(MediaPlayerDevice): - """Representation of a Cast device on the network.""" + """Representation of a Cast device on the network. - def __init__(self, chromecast): - """Initialize the Cast device.""" - self.cast = None # type: pychromecast.Chromecast + This class is the holder of the pychromecast.Chromecast object and its + socket client. It therefore handles all reconnects and audio group changing + "elected leader" itself. + """ + + def __init__(self, cast_info): + """Initialize the cast device.""" + self._cast_info = cast_info # type: ChromecastInfo + self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None self.media_status_received = None + self._available = False # type: bool + self._status_listener = None # type: Optional[CastStatusListener] - self.async_set_chromecast(chromecast) + async def async_added_to_hass(self): + """Create chromecast object when added to hass.""" + @callback + def async_cast_discovered(discover: ChromecastInfo): + """Callback for changing elected leaders / IP.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + self.hass.async_add_job(self.async_set_cast_info(discover)) + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect Chromecast object when removed.""" + self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + + async def async_set_cast_info(self, cast_info): + """Set the cast information and set up the chromecast object.""" + import pychromecast + old_cast_info = self._cast_info + self._cast_info = cast_info + + if self._chromecast is not None: + if old_cast_info.host_port == cast_info.host_port: + # Nothing connection-related updated + return + self._async_disconnect() + + # Failed connection will unfortunately never raise an exception, it + # will instead just try connecting indefinitely. + # pylint: disable=protected-access + _LOGGER.debug("Connecting to cast device %s", cast_info) + chromecast = await self.hass.async_add_job( + pychromecast._get_chromecast_from_host, attr.astuple(cast_info)) + self._chromecast = chromecast + self._status_listener = CastStatusListener(self, chromecast) + # Initialise connection status as connected because we can only + # register the connection listener *after* the initial connection + # attempt. If the initial connection failed, we would never reach + # this code anyway. + self._available = True + self.cast_status = chromecast.status + self.media_status = chromecast.media_controller.status + _LOGGER.debug("Connection successful!") + self.async_schedule_update_ha_state() + + @callback + def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self._chromecast is None: + # Can't disconnect if not connected. + return + _LOGGER.debug("Disconnecting from previous chromecast socket.") + self._available = False + self._chromecast.disconnect(blocking=False) + # Invalidate some attributes + self._chromecast = None + self.cast_status = None + self.media_status = None + self.media_status_received = None + self._status_listener.invalidate() + self._status_listener = None + + def update(self): + """Periodically update the properties. + + Even though we receive callbacks for most state changes, some 3rd party + apps don't always send them. Better poll every now and then if the + chromecast is active (i.e. an app is running). + """ + if not self._available: + # Not connected or not available. + return + + if self._chromecast.media_controller.is_active: + # We can only update status if the media namespace is active + self._chromecast.media_controller.update_status() + + # ========== Callbacks ========== + def new_cast_status(self, cast_status): + """Handle updates of the cast status.""" + self.cast_status = cast_status + self.schedule_update_ha_state() + + def new_media_status(self, media_status): + """Handle updates of the media status.""" + self.media_status = media_status + self.media_status_received = dt_util.utcnow() + self.schedule_update_ha_state() + + def new_connection_status(self, connection_status): + """Handle updates of connection status.""" + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED + + new_available = connection_status.status == CONNECTION_STATUS_CONNECTED + if new_available != self._available: + # Connection status callbacks happen often when disconnected. + # Only update state when availability changed to put less pressure + # on state machine. + _LOGGER.debug("Cast device availability changed: %s", + connection_status.status) + self._available = new_available + self.schedule_update_ha_state() + + # ========== Service Calls ========== + def turn_on(self): + """Turn on the cast device.""" + import pychromecast + + if not self._chromecast.is_idle: + # Already turned on + return + + if self._chromecast.app_id is not None: + # Quit the previous app before starting splash screen + self._chromecast.quit_app() + + # The only way we can turn the Chromecast is on is by launching an app + self._chromecast.play_media(CAST_SPLASH, + pychromecast.STREAM_TYPE_BUFFERED) + + def turn_off(self): + """Turn off the cast device.""" + self._chromecast.quit_app() + + def mute_volume(self, mute): + """Mute the volume.""" + self._chromecast.set_volume_muted(mute) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._chromecast.set_volume(volume) + + def media_play(self): + """Send play command.""" + self._chromecast.media_controller.play() + + def media_pause(self): + """Send pause command.""" + self._chromecast.media_controller.pause() + + def media_stop(self): + """Send stop command.""" + self._chromecast.media_controller.stop() + + def media_previous_track(self): + """Send previous track command.""" + self._chromecast.media_controller.rewind() + + def media_next_track(self): + """Send next track command.""" + self._chromecast.media_controller.skip() + + def media_seek(self, position): + """Seek the media to a specific location.""" + self._chromecast.media_controller.seek(position) + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL.""" + self._chromecast.media_controller.play_media(media_id, media_type) + + # ========== Properties ========== @property def should_poll(self): - """No polling needed.""" - return False + """Polling needed for cast integration, see async_update.""" + return True @property def name(self): """Return the name of the device.""" - return self.cast.device.friendly_name + return self._cast_info.friendly_name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the player.""" if self.media_status is None: - return STATE_UNKNOWN + return None elif self.media_status.player_is_playing: return STATE_PLAYING elif self.media_status.player_is_paused: return STATE_PAUSED elif self.media_status.player_is_idle: return STATE_IDLE - elif self.cast.is_idle: + elif self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF - return STATE_UNKNOWN + return None + + @property + def available(self): + """Return True if the cast device is connected.""" + return self._available @property def volume_level(self): @@ -318,12 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def app_id(self): """Return the ID of the current running app.""" - return self.cast.app_id + return self._chromecast.app_id if self._chromecast else None @property def app_name(self): """Name of the current running app.""" - return self.cast.app_display_name + return self._chromecast.app_display_name if self._chromecast else None @property def supported_features(self): @@ -349,101 +611,7 @@ class CastDevice(MediaPlayerDevice): """ return self.media_status_received - def turn_on(self): - """Turn on the ChromeCast.""" - # The only way we can turn the Chromecast is on is by launching an app - if not self.cast.status or not self.cast.status.is_active_input: - import pychromecast - - if self.cast.app_id: - self.cast.quit_app() - - self.cast.play_media( - CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) - - def turn_off(self): - """Turn Chromecast off.""" - self.cast.quit_app() - - def mute_volume(self, mute): - """Mute the volume.""" - self.cast.set_volume_muted(mute) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self.cast.set_volume(volume) - - def media_play(self): - """Send play command.""" - self.cast.media_controller.play() - - def media_pause(self): - """Send pause command.""" - self.cast.media_controller.pause() - - def media_stop(self): - """Send stop command.""" - self.cast.media_controller.stop() - - def media_previous_track(self): - """Send previous track command.""" - self.cast.media_controller.rewind() - - def media_next_track(self): - """Send next track command.""" - self.cast.media_controller.skip() - - def media_seek(self, position): - """Seek the media to a specific location.""" - self.cast.media_controller.seek(position) - - def play_media(self, media_type, media_id, **kwargs): - """Play media from a URL.""" - self.cast.media_controller.play_media(media_id, media_type) - - # Implementation of chromecast status_listener methods - def new_cast_status(self, status): - """Handle updates of the cast status.""" - self.cast_status = status - self.schedule_update_ha_state() - - def new_media_status(self, status): - """Handle updates of the media status.""" - self.media_status = status - self.media_status_received = dt_util.utcnow() - self.schedule_update_ha_state() - @property - def unique_id(self) -> str: + def unique_id(self) -> Optional[str]: """Return a unique ID.""" - if self.cast.uuid is not None: - return str(self.cast.uuid) - return None - - @callback - def async_set_chromecast(self, chromecast): - """Set the internal Chromecast object and disconnect the previous.""" - self._async_disconnect() - - self.cast = chromecast - - self.cast.socket_client.receiver_controller.register_status_listener( - self) - self.cast.socket_client.media_controller.register_status_listener(self) - - self.cast_status = self.cast.status - self.media_status = self.cast.media_controller.status - - async def async_will_remove_from_hass(self) -> None: - """Disconnect Chromecast object when removed.""" - self._async_disconnect() - - @callback - def _async_disconnect(self): - """Disconnect Chromecast object if it is set.""" - if self.cast is None: - return - _LOGGER.debug("Disconnecting existing chromecast object") - old_key = (self.cast.host, self.cast.port, self.cast.uuid) - self.hass.data[KNOWN_CHROMECASTS_KEY].pop(old_key) - self.cast.disconnect(blocking=False) + return self._cast_info.uuid diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index eda47237b44..480e5152c8e 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -45,7 +45,7 @@ SERVICE_SEEK_BY = 'channels_seek_by' ATTR_SECONDS = 'seconds' CHANNELS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, }) CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ @@ -55,14 +55,12 @@ CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ REQUIREMENTS = ['pychannels==1.0.0'] -# pylint: disable=unused-argument, abstract-method -# pylint: disable=too-many-instance-attributes def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Channels platform.""" device = ChannelsPlayer( - config.get('name', DEFAULT_NAME), + config.get('name'), config.get(CONF_HOST), - config.get(CONF_PORT, DEFAULT_PORT) + config.get(CONF_PORT) ) if DATA_CHANNELS not in hass.data: @@ -73,22 +71,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def service_handler(service): """Handler for services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) + entity_id = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [device for device in hass.data[DATA_CHANNELS] - if device.entity_id in entity_ids] - else: - devices = hass.data[DATA_CHANNELS] + device = next((device for device in hass.data[DATA_CHANNELS] if + device.entity_id == entity_id), None) - for device in devices: - if service.service == SERVICE_SEEK_FORWARD: - device.seek_forward() - elif service.service == SERVICE_SEEK_BACKWARD: - device.seek_backward() - elif service.service == SERVICE_SEEK_BY: - seconds = service.data.get('seconds') - device.seek_by(seconds) + if device is None: + _LOGGER.warning("Unable to find Channels with entity_id: %s", + entity_id) + return + + if service.service == SERVICE_SEEK_FORWARD: + device.seek_forward() + elif service.service == SERVICE_SEEK_BACKWARD: + device.seek_backward() + elif service.service == SERVICE_SEEK_BY: + seconds = service.data.get('seconds') + device.seek_by(seconds) hass.services.register( DOMAIN, SERVICE_SEEK_FORWARD, service_handler, diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 6450b2f5b35..33116258978 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -73,6 +73,8 @@ MEDIA_TYPES = { 'episode': MEDIA_TYPE_TVSHOW, # Type 'channel' is used for radio or tv streams from pvr 'channel': MEDIA_TYPE_CHANNEL, + # Type 'audio' is used for audio media, that Kodi couldn't scroblle + 'audio': MEDIA_TYPE_MUSIC, } SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -480,7 +482,12 @@ class KodiDevice(MediaPlayerDevice): @property def media_content_type(self): - """Content type of current playing media.""" + """Content type of current playing media. + + If the media type cannot be detected, the player type is used. + """ + if MEDIA_TYPES.get(self._item.get('type')) is None and self._players: + return MEDIA_TYPES.get(self._players[0]['type']) return MEDIA_TYPES.get(self._item.get('type')) @property diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index 3cf0ecdb232..a6d5841bb0f 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -9,134 +9,182 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, - MediaPlayerDevice) + MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_MUTE, MediaPlayerDevice, +) +from homeassistant.helpers.dispatcher import ( + dispatcher_send, async_dispatcher_connect +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, - STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_ON) + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, + CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, + STATE_UNAVAILABLE +) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymediaroom==0.5'] + +REQUIREMENTS = ['pymediaroom==0.6'] _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = 'Mediaroom Media Player Setup' -NOTIFICATION_ID = 'mediaroom_notification' DEFAULT_NAME = 'Mediaroom STB' DEFAULT_TIMEOUT = 9 DATA_MEDIAROOM = "mediaroom_known_stb" +DISCOVERY_MEDIAROOM = "mediaroom_discovery_installed" +SIGNAL_STB_NOTIFY = 'mediaroom_stb_discovered' +SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_PLAY_MEDIA \ + | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ + | SUPPORT_PLAY -SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Mediaroom platform.""" - hosts = [] - known_hosts = hass.data.get(DATA_MEDIAROOM) if known_hosts is None: known_hosts = hass.data[DATA_MEDIAROOM] = [] - host = config.get(CONF_HOST, None) - if host is None: - _LOGGER.info("Trying to discover Mediaroom STB") + if host: + async_add_devices([MediaroomDevice(host=host, + device_id=None, + optimistic=config[CONF_OPTIMISTIC], + timeout=config[CONF_TIMEOUT])]) + hass.data[DATA_MEDIAROOM].append(host) - from pymediaroom import Remote + _LOGGER.debug("Trying to discover Mediaroom STB") - host = Remote.discover(known_hosts) - if host is None: - _LOGGER.warning("Can't find any STB") + def callback_notify(notify): + """Process NOTIFY message from STB.""" + if notify.ip_address in hass.data[DATA_MEDIAROOM]: + dispatcher_send(hass, SIGNAL_STB_NOTIFY, notify) return - hosts.append(host) - known_hosts.append(host) - stbs = [] + _LOGGER.debug("Discovered new stb %s", notify.ip_address) + hass.data[DATA_MEDIAROOM].append(notify.ip_address) + new_stb = MediaroomDevice( + host=notify.ip_address, device_id=notify.device_uuid, + optimistic=False + ) + async_add_devices([new_stb]) - try: - for host in hosts: - stbs.append(MediaroomDevice( - config.get(CONF_NAME), - host, - config.get(CONF_OPTIMISTIC), - config.get(CONF_TIMEOUT) - )) + if not config[CONF_OPTIMISTIC]: + from pymediaroom import install_mediaroom_protocol - except ConnectionRefusedError: - hass.components.persistent_notification.create( - 'Error: Unable to initialize mediaroom at {}
' - 'Check its network connection or consider ' - 'using auto discovery.
' - 'You will need to restart hass after fixing.' - ''.format(host), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - add_devices(stbs) + already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False) + if not already_installed: + await install_mediaroom_protocol( + responses_callback=callback_notify) + _LOGGER.debug("Auto discovery installed") + hass.data[DISCOVERY_MEDIAROOM] = True class MediaroomDevice(MediaPlayerDevice): """Representation of a Mediaroom set-up-box on the network.""" - def __init__(self, name, host, optimistic=False, timeout=DEFAULT_TIMEOUT): + def set_state(self, mediaroom_state): + """Helper method to map pymediaroom states to HA states.""" + from pymediaroom import State + + state_map = { + State.OFF: STATE_OFF, + State.STANDBY: STATE_STANDBY, + State.PLAYING_LIVE_TV: STATE_PLAYING, + State.PLAYING_RECORDED_TV: STATE_PLAYING, + State.PLAYING_TIMESHIFT_TV: STATE_PLAYING, + State.STOPPED: STATE_PAUSED, + State.UNKNOWN: STATE_UNAVAILABLE + } + + self._state = state_map[mediaroom_state] + + def __init__(self, host, device_id, optimistic=False, + timeout=DEFAULT_TIMEOUT): """Initialize the device.""" from pymediaroom import Remote - self.stb = Remote(host, timeout=timeout) - _LOGGER.info( - "Found %s at %s%s", name, host, - " - I'm optimistic" if optimistic else "") - self._name = name - self._is_standby = not optimistic - self._current = None + self.host = host + self.stb = Remote(host) + _LOGGER.info("Found STB at %s%s", host, + " - I'm optimistic" if optimistic else "") + self._channel = None self._optimistic = optimistic - self._state = STATE_STANDBY + self._state = STATE_PLAYING if optimistic else STATE_STANDBY + self._name = 'Mediaroom {}'.format(device_id) + self._available = True + if device_id: + self._unique_id = device_id + else: + self._unique_id = None - def update(self): + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + async def async_added_to_hass(self): """Retrieve latest state.""" - if not self._optimistic: - self._is_standby = self.stb.get_standby() - if self._is_standby: - self._state = STATE_STANDBY - elif self._state not in [STATE_PLAYING, STATE_PAUSED]: - self._state = STATE_PLAYING - _LOGGER.debug( - "%s(%s) is [%s]", - self._name, self.stb.stb_ip, self._state) + async def async_notify_received(notify): + """Process STB state from NOTIFY message.""" + stb_state = self.stb.notify_callback(notify) + # stb_state is None in case the notify is not from the current stb + if not stb_state: + return + self.set_state(stb_state) + _LOGGER.debug("STB(%s) is [%s]", self.host, self._state) + self._available = True + self.async_schedule_update_ha_state() - def play_media(self, media_type, media_id, **kwargs): + async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, + async_notify_received) + + async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - _LOGGER.debug( - "%s(%s) Play media: %s (%s)", - self._name, self.stb.stb_ip, media_id, media_type) + from pymediaroom import PyMediaroomError + + _LOGGER.debug("STB(%s) Play media: %s (%s)", self.stb.stb_ip, + media_id, media_type) if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error('invalid media type') return - if media_id.isdigit(): - media_id = int(media_id) - else: + if not media_id.isdigit(): + _LOGGER.error("media_id must be a channel number") return - self.stb.send_cmd(media_id) - self._state = STATE_PLAYING + + try: + await self.stb.send_cmd(int(media_id)) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): """Return the name of the device.""" return self._name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the device.""" @@ -152,50 +200,120 @@ class MediaroomDevice(MediaPlayerDevice): """Return the content type of current playing media.""" return MEDIA_TYPE_CHANNEL - def turn_on(self): + @property + def media_channel(self): + """Channel currently playing.""" + return self._channel + + async def async_turn_on(self): """Turn on the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_ON + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_on()) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def turn_off(self): + async def async_turn_off(self): """Turn off the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_STANDBY + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_off()) + if self._optimistic: + self._state = STATE_STANDBY + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_play(self): + async def async_media_play(self): """Send play command.""" - _LOGGER.debug("media_play()") - self.stb.send_cmd('PlayPause') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + _LOGGER.debug("media_play()") + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_pause(self): + async def async_media_pause(self): """Send pause command.""" - self.stb.send_cmd('PlayPause') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" - self.stb.send_cmd('Stop') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Stop') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_previous_track(self): + async def async_media_previous_track(self): """Send Program Down command.""" - self.stb.send_cmd('ProgDown') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgDown') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_next_track(self): + async def async_media_next_track(self): """Send Program Up command.""" - self.stb.send_cmd('ProgUp') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgUp') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - self.stb.send_cmd('VolUp') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolUp') + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_down(self): + async def async_volume_down(self): """Send volume up command.""" - self.stb.send_cmd('VolDown') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolDown') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - self.stb.send_cmd('Mute') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Mute') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index cc195db2590..a375a585ad4 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -155,8 +155,8 @@ class MpcHcDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._send_command(921) + self._send_command(920) def media_previous_track(self): """Send previous track command.""" - self._send_command(920) + self._send_command(919) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 24981555007..29d336e4d7a 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -15,11 +15,11 @@ from homeassistant.components.media_player import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) + CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.1'] +REQUIREMENTS = ['ha-philipsjs==0.0.2'] _LOGGER = logging.getLogger(__name__) @@ -36,10 +36,12 @@ CONF_ON_ACTION = 'turn_on_action' DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = 'Philips TV' +DEFAULT_API_VERSION = '1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, }) @@ -51,9 +53,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) + api_version = config.get(CONF_API_VERSION) turn_on_action = config.get(CONF_ON_ACTION) - tvapi = haphilipsjs.PhilipsTV(host) + tvapi = haphilipsjs.PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None add_devices([PhilipsTV(tvapi, name, on_script)]) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 48e532074f7..edb8aa147fb 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -23,6 +23,8 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.json import load_json, save_json +from homeassistant.util import dt as dt_util + REQUIREMENTS = ['plexapi==3.0.6'] @@ -38,6 +40,8 @@ CONF_INCLUDE_NON_CLIENTS = 'include_non_clients' CONF_USE_EPISODE_ART = 'use_episode_art' CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids' CONF_SHOW_ALL_CONTROLS = 'show_all_controls' +CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' +CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): @@ -46,6 +50,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ cv.boolean, vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, + vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): + cv.boolean, + vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): + vol.All(cv.time_period, cv.positive_timedelta), }) PLEX_DATA = "plex" @@ -184,6 +192,7 @@ def setup_plexserver( else: plex_clients[machine_identifier].refresh(None, session) + clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -192,6 +201,18 @@ def setup_plexserver( client.set_availability(client.machine_identifier in available_client_ids) + if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \ + or client.available: + continue + + if (dt_util.utcnow() - client.marked_unavailable) >= \ + (config.get(CONF_CLIENT_REMOVE_INTERVAL)): + hass.add_job(client.async_remove()) + clients_to_remove.append(client.machine_identifier) + + while clients_to_remove: + del plex_clients[clients_to_remove.pop()] + if new_plex_clients: add_devices_callback(new_plex_clients) @@ -266,6 +287,7 @@ class PlexClient(MediaPlayerDevice): self._app_name = '' self._device = None self._available = False + self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False self._is_player_available = False @@ -418,6 +440,11 @@ class PlexClient(MediaPlayerDevice): """Set the device as available/unavailable noting time.""" if not available: self._clear_media_details() + if self._marked_unavailable is None: + self._marked_unavailable = dt_util.utcnow() + else: + self._marked_unavailable = None + self._available = available def _set_player_state(self): @@ -506,6 +533,11 @@ class PlexClient(MediaPlayerDevice): """Return the device, if any.""" return self._device + @property + def marked_unavailable(self): + """Return time device was marked unavailable.""" + return self._marked_unavailable + @property def session(self): """Return the session, if any.""" diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index beaea8a8ad0..95072f0270c 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -131,8 +131,8 @@ play_media: description: The ID of the content to play. Platform dependent. example: 'https://home-assistant.io/images/cast/splash.png' media_content_type: - description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST - example: 'MUSIC' + description: The type of the content to play. Must be one of music, tvshow, video, episode, channel or playlist + example: 'music' select_source: description: Send the media player the command to change input source. diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index b1dc7df3319..e43f5951db7 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -154,7 +154,7 @@ class SongpalDevice(MediaPlayerDevice): _LOGGER.warning("Got %s volume controls, using the first one", volumes) - volume = volumes.pop() + volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) self._volume_max = volume.maxVolume diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 448c66c4e45..b10c761d532 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -54,7 +54,6 @@ DATA_SONOS = 'sonos' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' -CONF_ADVERTISE_ADDR = 'advertise_addr' CONF_INTERFACE_ADDR = 'interface_addr' # Service call validation schemas @@ -73,7 +72,6 @@ ATTR_IS_COORDINATOR = 'is_coordinator' UPNP_ERRORS_TO_IGNORE = ['701', '711'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ADVERTISE_ADDR): cv.string, vol.Optional(CONF_INTERFACE_ADDR): cv.string, vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), }) @@ -141,10 +139,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() - advertise_addr = config.get(CONF_ADVERTISE_ADDR, None) - if advertise_addr: - soco.config.EVENT_ADVERTISE_IP = advertise_addr - + players = [] if discovery_info: player = soco.SoCo(discovery_info.get('host')) @@ -152,25 +147,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if player.uid in hass.data[DATA_SONOS].uids: return - if player.is_visible: - hass.data[DATA_SONOS].uids.add(player.uid) - add_devices([SonosDevice(player)]) + # If invisible, such as a stereo slave + if not player.is_visible: + return + + players.append(player) else: - players = None - hosts = config.get(CONF_HOSTS, None) + hosts = config.get(CONF_HOSTS) if hosts: # Support retro compatibility with comma separated list of hosts # from config hosts = hosts[0] if len(hosts) == 1 else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts - players = [] for host in hosts: try: players.append(soco.SoCo(socket.gethostbyname(host))) except OSError: _LOGGER.warning("Failed to initialize '%s'", host) - - if not players: + else: players = soco.discover( interface_addr=config.get(CONF_INTERFACE_ADDR)) @@ -178,9 +172,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.warning("No Sonos speakers found") return - hass.data[DATA_SONOS].uids.update([p.uid for p in players]) - add_devices([SonosDevice(p) for p in players]) - _LOGGER.debug("Added %s Sonos speakers", len(players)) + hass.data[DATA_SONOS].uids.update(p.uid for p in players) + add_devices(SonosDevice(p) for p in players) + _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" @@ -214,9 +208,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif service.service == SERVICE_CLEAR_TIMER: device.clear_sleep_timer() elif service.service == SERVICE_UPDATE_ALARM: - device.update_alarm(**service.data) + device.set_alarm(**service.data) elif service.service == SERVICE_SET_OPTION: - device.update_option(**service.data) + device.set_option(**service.data) device.schedule_update_ha_state(True) @@ -327,7 +321,7 @@ def _is_radio_uri(uri): """Return whether the URI is a radio stream.""" radio_schemes = ( 'x-rincon-mp3radio:', 'x-sonosapi-stream:', 'x-sonosapi-radio:', - 'hls-radio:') + 'x-sonosapi-hls:', 'hls-radio:') return uri.startswith(radio_schemes) @@ -336,12 +330,13 @@ class SonosDevice(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos device.""" + self._receives_events = False self._volume_increment = 5 self._unique_id = player.uid self._player = player self._model = None self._player_volume = None - self._player_volume_muted = None + self._player_muted = None self._play_mode = None self._name = None self._coordinator = None @@ -426,11 +421,9 @@ class SonosDevice(MediaPlayerDevice): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] - self._player_volume = self.soco.volume - self._player_volume_muted = self.soco.mute self._play_mode = self.soco.play_mode - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode + + self.update_volume() self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): @@ -443,124 +436,6 @@ class SonosDevice(MediaPlayerDevice): except Exception: _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) - def _subscribe_to_player_events(self): - """Add event subscriptions.""" - player = self.soco - - # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.process_zonegrouptopology_event(None) - - queue = _ProcessSonosEventQueue(self.process_avtransport_event) - player.avTransport.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_rendering_event) - player.renderingControl.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_zonegrouptopology_event) - player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) - - def update(self): - """Retrieve latest state.""" - available = self._check_available() - if self._available != available: - self._available = available - if available: - self._set_basic_information() - self._subscribe_to_player_events() - else: - self._player_volume = None - self._player_volume_muted = None - self._status = 'OFF' - self._coordinator = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._source_name = None - - def process_avtransport_event(self, event): - """Process a track change event coming from a coordinator.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info.get('current_transport_state') - - # Ignore transitions, we should get the target state soon - if new_status == 'TRANSITIONING': - return - - self._play_mode = self.soco.play_mode - - if self.soco.is_playing_tv: - self._refresh_linein(SOURCE_TV) - elif self.soco.is_playing_line_in: - self._refresh_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - - if _is_radio_uri(track_info['uri']): - self._refresh_radio(event.variables, track_info) - else: - update_position = (new_status != self._status) - self._refresh_music(update_position, track_info) - - self._status = new_status - - self.schedule_update_ha_state() - - # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: - coordinator = entity.coordinator - if coordinator and coordinator.unique_id == self.unique_id: - entity.schedule_update_ha_state() - - def process_rendering_event(self, event): - """Process a volume change event coming from a player.""" - variables = event.variables - - if 'volume' in variables: - self._player_volume = int(variables['volume']['Master']) - - if 'mute' in variables: - self._player_volume_muted = (variables['mute']['Master'] == '1') - - if 'night_mode' in variables: - self._night_sound = (variables['night_mode'] == '1') - - if 'dialog_level' in variables: - self._speech_enhance = (variables['dialog_level'] == '1') - - self.schedule_update_ha_state() - - def process_zonegrouptopology_event(self, event): - """Process a zone group topology event coming from a player.""" - if event and not hasattr(event, 'zone_player_uui_ds_in_group'): - return - - with self.hass.data[DATA_SONOS].topology_lock: - group = event and event.zone_player_uui_ds_in_group - if group: - # New group information is pushed - coordinator_uid, *slave_uids = group.split(',') - else: - # Use SoCo cache for existing topology - coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [p.uid for p in self.soco.group.members - if p.uid != coordinator_uid] - - if self.unique_id == coordinator_uid: - self._coordinator = None - self.schedule_update_ha_state() - - for slave_uid in slave_uids: - slave = _get_entity_from_soco_uid(self.hass, slave_uid) - if slave: - # pylint: disable=protected-access - slave._coordinator = self - slave.schedule_update_ha_state() - def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" if url not in ('', 'NOT_IMPLEMENTED', None): @@ -574,7 +449,88 @@ class SonosDevice(MediaPlayerDevice): ) return url - def _refresh_linein(self, source): + def _subscribe_to_player_events(self): + """Add event subscriptions.""" + self._receives_events = False + + # New player available, build the current group topology + for device in self.hass.data[DATA_SONOS].devices: + device.update_groups() + + player = self.soco + + queue = _ProcessSonosEventQueue(self.update_media) + player.avTransport.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_volume) + player.renderingControl.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_groups) + player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) + + def update(self): + """Retrieve latest state.""" + available = self._check_available() + if self._available != available: + self._available = available + if available: + self._set_basic_information() + self._subscribe_to_player_events() + else: + self._player_volume = None + self._player_muted = None + self._status = 'OFF' + self._coordinator = None + self._media_duration = None + self._media_position = None + self._media_position_updated_at = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None + self._source_name = None + elif available and not self._receives_events: + self.update_groups() + self.update_volume() + if self.is_coordinator: + self.update_media() + + def update_media(self, event=None): + """Update information about currently playing media.""" + transport_info = self.soco.get_current_transport_info() + new_status = transport_info.get('current_transport_state') + + # Ignore transitions, we should get the target state soon + if new_status == 'TRANSITIONING': + return + + self._play_mode = self.soco.play_mode + + if self.soco.is_playing_tv: + self.update_media_linein(SOURCE_TV) + elif self.soco.is_playing_line_in: + self.update_media_linein(SOURCE_LINEIN) + else: + track_info = self.soco.get_current_track_info() + + if _is_radio_uri(track_info['uri']): + variables = event and event.variables + self.update_media_radio(variables, track_info) + else: + update_position = (new_status != self._status) + self.update_media_music(update_position, track_info) + + self._status = new_status + + self.schedule_update_ha_state() + + # Also update slaves + for entity in self.hass.data[DATA_SONOS].devices: + coordinator = entity.coordinator + if coordinator and coordinator.unique_id == self.unique_id: + entity.schedule_update_ha_state() + + def update_media_linein(self, source): """Update state when playing from line-in/tv.""" self._media_duration = None self._media_position = None @@ -588,7 +544,7 @@ class SonosDevice(MediaPlayerDevice): self._source_name = source - def _refresh_radio(self, variables, track_info): + def update_media_radio(self, variables, track_info): """Update state when streaming radio.""" self._media_duration = None self._media_position = None @@ -609,7 +565,7 @@ class SonosDevice(MediaPlayerDevice): artist=self._media_artist, title=self._media_title ) - else: + elif variables: # "On Now" field in the sonos pc app current_track_metadata = variables.get('current_track_meta_data') if current_track_metadata: @@ -649,7 +605,7 @@ class SonosDevice(MediaPlayerDevice): if fav.reference.get_uri() == media_info['CurrentURI']: self._source_name = fav.title - def _refresh_music(self, update_media_position, track_info): + def update_media_music(self, update_media_position, track_info): """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get('duration')) @@ -688,6 +644,60 @@ class SonosDevice(MediaPlayerDevice): self._source_name = None + def update_volume(self, event=None): + """Update information about currently volume settings.""" + if event: + variables = event.variables + + if 'volume' in variables: + self._player_volume = int(variables['volume']['Master']) + + if 'mute' in variables: + self._player_muted = (variables['mute']['Master'] == '1') + + if 'night_mode' in variables: + self._night_sound = (variables['night_mode'] == '1') + + if 'dialog_level' in variables: + self._speech_enhance = (variables['dialog_level'] == '1') + + self.schedule_update_ha_state() + else: + self._player_volume = self.soco.volume + self._player_muted = self.soco.mute + self._night_sound = self.soco.night_mode + self._speech_enhance = self.soco.dialog_mode + + def update_groups(self, event=None): + """Process a zone group topology event coming from a player.""" + if event: + self._receives_events = True + + if not hasattr(event, 'zone_player_uui_ds_in_group'): + return + + with self.hass.data[DATA_SONOS].topology_lock: + group = event and event.zone_player_uui_ds_in_group + if group: + # New group information is pushed + coordinator_uid, *slave_uids = group.split(',') + else: + # Use SoCo cache for existing topology + coordinator_uid = self.soco.group.coordinator.uid + slave_uids = [p.uid for p in self.soco.group.members + if p.uid != coordinator_uid] + + if self.unique_id == coordinator_uid: + self._coordinator = None + self.schedule_update_ha_state() + + for slave_uid in slave_uids: + slave = _get_entity_from_soco_uid(self.hass, slave_uid) + if slave: + # pylint: disable=protected-access + slave._coordinator = self + slave.schedule_update_ha_state() + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -696,7 +706,7 @@ class SonosDevice(MediaPlayerDevice): @property def is_volume_muted(self): """Return true if volume is muted.""" - return self._player_volume_muted + return self._player_muted @property @soco_coordinator @@ -994,7 +1004,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() @soco_coordinator - def update_alarm(self, **data): + def set_alarm(self, **data): """Set the alarm clock on the player.""" from soco import alarms alarm = None @@ -1017,7 +1027,7 @@ class SonosDevice(MediaPlayerDevice): alarm.save() @soco_error() - def update_option(self, **data): + def set_option(self, **data): """Modify playback options.""" if ATTR_NIGHT_SOUND in data and self._night_sound is not None: self.soco.night_mode = data[ATTR_NIGHT_SOUND] diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 734285d918a..963258f1861 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -194,7 +194,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._title = item.get('name') self._artist = ', '.join([artist.get('name') for artist in item.get('artists')]) - self._uri = current.get('uri') + self._uri = item.get('uri') images = item.get('album').get('images') self._image_url = images[0].get('url') if images else None # Playing state diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 27e7c0358ad..b81a4fc16a7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -27,7 +27,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, @@ -515,16 +515,20 @@ class MQTT(object): This method is a coroutine. """ result = None # type: int - result = await self.hass.async_add_job( - self._mqttc.connect, self.broker, self.port, self.keepalive) + try: + result = await self.hass.async_add_job( + self._mqttc.connect, self.broker, self.port, self.keepalive) + except OSError as err: + _LOGGER.error('Failed to connect due to exception: %s', err) + return False if result != 0: import paho.mqtt.client as mqtt _LOGGER.error('Failed to connect: %s', mqtt.error_string(result)) - else: - self._mqttc.loop_start() + return False - return not result + self._mqttc.loop_start() + return True @callback def async_disconnect(self): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index b6f6a1c5a92..3263521f3f1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,7 +4,6 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ -import asyncio import json import logging import re @@ -21,13 +20,14 @@ TOPIC_MATCHER = re.compile( r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config') SUPPORTED_COMPONENTS = [ - 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch'] + 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], 'cover': ['mqtt'], 'fan': ['mqtt'], 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], + 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], } @@ -35,19 +35,16 @@ ALLOWED_PLATFORMS = { ALREADY_DISCOVERED = 'mqtt_discovered_components' -@asyncio.coroutine -def async_start(hass, discovery_topic, hass_config): +async def async_start(hass, discovery_topic, hass_config): """Initialize of MQTT Discovery.""" - # pylint: disable=unused-variable - @asyncio.coroutine - def async_device_message_received(topic, payload, qos): + async def async_device_message_received(topic, payload, qos): """Process the received message.""" match = TOPIC_MATCHER.match(topic) if not match: return - prefix_topic, component, node_id, object_id = match.groups() + _prefix_topic, component, node_id, object_id = match.groups() try: payload = json.loads(payload) @@ -88,10 +85,10 @@ def async_start(hass, discovery_topic, hass_config): _LOGGER.info("Found new component: %s %s", component, discovery_id) - yield from async_load_platform( + await async_load_platform( hass, component, platform, payload, hass_config) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 37e257e5eb9..a560b49648f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -545,9 +545,8 @@ def setup_mysensors_platform( 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) + 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: diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 37028decf71..e7d2ba90438 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['python-nest==3.1.0'] +REQUIREMENTS = ['python-nest==3.7.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f4c9c391408..895ffd9db10 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/notify.lametric/ """ import logging +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.notify import ( @@ -49,6 +50,7 @@ class LaMetricNotificationService(BaseNotificationService): self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._devices = [] # pylint: disable=broad-except def send_message(self, message="", **kwargs): @@ -86,12 +88,15 @@ class LaMetricNotificationService(BaseNotificationService): model = Model(frames=frames, cycles=cycles, sound=sound) lmn = self.hasslametricmanager.manager try: - devices = lmn.get_devices() + self._devices = lmn.get_devices() except TokenExpiredError: _LOGGER.debug("Token expired, fetching new token") lmn.get_token() - devices = lmn.get_devices() - for dev in devices: + self._devices = lmn.get_devices() + except RequestsConnectionError: + _LOGGER.warning("Problem connecting to LaMetric, " + "using cached devices instead") + for dev in self._devices: if targets is None or dev["name"] in targets: try: lmn.set_device(dev) diff --git a/homeassistant/components/notify/stride.py b/homeassistant/components/notify/stride.py new file mode 100644 index 00000000000..f31e50a5886 --- /dev/null +++ b/homeassistant/components/notify/stride.py @@ -0,0 +1,102 @@ +""" +Stride platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.stride/ +""" +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_ROOM + +REQUIREMENTS = ['pystride==0.1.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PANEL = 'panel' +CONF_CLOUDID = 'cloudid' + +DEFAULT_PANEL = None + +VALID_PANELS = {'info', 'note', 'tip', 'warning', None} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLOUDID): cv.string, + vol.Required(CONF_ROOM): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Stride notification service.""" + return StrideNotificationService( + config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], + config[CONF_CLOUDID]) + + +class StrideNotificationService(BaseNotificationService): + """Implement the notification service for Stride.""" + + def __init__(self, token, default_room, default_panel, cloudid): + """Initialize the service.""" + self._token = token + self._default_room = default_room + self._default_panel = default_panel + self._cloudid = cloudid + + from stride import Stride + self._stride = Stride(self._cloudid, access_token=self._token) + + def send_message(self, message="", **kwargs): + """Send a message.""" + panel = self._default_panel + + if kwargs.get(ATTR_DATA) is not None: + data = kwargs.get(ATTR_DATA) + if ((data.get(CONF_PANEL) is not None) + and (data.get(CONF_PANEL) in VALID_PANELS)): + panel = data.get(CONF_PANEL) + + message_text = { + 'type': 'paragraph', + 'content': [ + { + 'type': 'text', + 'text': message + } + ] + } + panel_text = message_text + if panel is not None: + panel_text = { + 'type': 'panel', + 'attrs': + { + 'panelType': panel + }, + 'content': + [ + message_text, + ] + } + + message_doc = { + 'body': { + 'version': 1, + 'type': 'doc', + 'content': + [ + panel_text, + ] + } + } + + targets = kwargs.get(ATTR_TARGET, [self._default_room]) + + for target in targets: + self._stride.message_room(target, message_doc) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index c6f4fa0dd5f..9489e05cfa5 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.4.8'] +REQUIREMENTS = ['TwitterAPI==2.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 392bccb56d4..f10e0fc75d7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -29,12 +29,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from homeassistant.loader import bind_hass from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] _LOGGER = logging.getLogger(__name__) @@ -63,16 +64,13 @@ CONNECT_RETRY_WAIT = 3 FILTER_SCHEMA = vol.Schema({ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EVENT_TYPES): - vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string]), }), vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]) }) }) @@ -87,14 +85,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def wait_connection_ready(hass): - """ - Wait till the connection is ready. - - Returns a coroutine object. - """ - return (yield from hass.data[DATA_INSTANCE].async_db_ready) +@bind_hass +async def wait_connection_ready(hass): + """Wait till the connection is ready.""" + return await hass.data[DATA_INSTANCE].async_db_ready def run_information(hass, point_in_time: Optional[datetime] = None): @@ -258,7 +252,7 @@ class Recorder(threading.Thread): self.hass.add_job(register) result = hass_started.result() - # If shutdown happened before HASS finished starting + # If shutdown happened before Home Assistant finished starting if result is shutdown_task: return diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 924556a039d..b71eb2cb447 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -18,10 +18,11 @@ from homeassistant.components.remote import ( from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) @@ -78,10 +79,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Check that we can communicate with device. try: - device.info() + device_info = device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) except DeviceException as ex: - _LOGGER.error("Token not accepted by device : %s", ex) - return + _LOGGER.error("Device unavailable or token incorrect: %s", ex) + raise PlatformNotReady if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -93,9 +100,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hidden = config.get(ATTR_HIDDEN) - xiaomi_miio_remote = XiaomiMiioRemote( - friendly_name, device, slot, timeout, - hidden, config.get(CONF_COMMANDS)) + xiaomi_miio_remote = XiaomiMiioRemote(friendly_name, device, unique_id, + slot, timeout, hidden, + config.get(CONF_COMMANDS)) hass.data[DATA_KEY][host] = xiaomi_miio_remote @@ -158,17 +165,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class XiaomiMiioRemote(RemoteDevice): """Representation of a Xiaomi Miio Remote device.""" - def __init__(self, friendly_name, device, + def __init__(self, friendly_name, device, unique_id, slot, timeout, hidden, commands): """Initialize the remote.""" self._name = friendly_name self._device = device + self._unique_id = unique_id self._is_hidden = hidden self._slot = slot self._timeout = timeout self._state = False self._commands = commands + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the remote.""" diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py index 6e70ddb244d..1a15e22fca0 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring.py @@ -37,8 +37,8 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Ring component.""" conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] try: from ring_doorbell import Ring diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index db81d84c2b7..dffc7720776 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.scene import Scene @@ -13,8 +11,8 @@ from homeassistant.components.scene import Scene DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up scenes for deCONZ component.""" if discovery_info is None: return @@ -34,15 +32,13 @@ class DeconzScene(Scene): """Set up a scene.""" self._scene = scene - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" - yield from self._scene.async_set_state({}) + await self._scene.async_set_state({}) @property def name(self): diff --git a/homeassistant/components/sensor/.translations/season.cs.json b/homeassistant/components/sensor/.translations/season.cs.json new file mode 100644 index 00000000000..e2d7e7919be --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Podzim", + "spring": "Jaro", + "summer": "L\u00e9to", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.cy.json b/homeassistant/components/sensor/.translations/season.cy.json new file mode 100644 index 00000000000..0d1553ac3ea --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cy.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hydref", + "spring": "Gwanwyn", + "summer": "Haf", + "winter": "Gaeaf" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.de.json b/homeassistant/components/sensor/.translations/season.de.json new file mode 100644 index 00000000000..50d702340b9 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herbst", + "spring": "Fr\u00fchling", + "summer": "Sommer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.es.json b/homeassistant/components/sensor/.translations/season.es.json new file mode 100644 index 00000000000..65df6a58b10 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Oto\u00f1o", + "spring": "Primavera", + "summer": "Verano", + "winter": "Invierno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.fi.json b/homeassistant/components/sensor/.translations/season.fi.json new file mode 100644 index 00000000000..f01f6451549 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Syksy", + "spring": "Kev\u00e4t", + "summer": "Kes\u00e4", + "winter": "Talvi" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ja.json b/homeassistant/components/sensor/.translations/season.ja.json new file mode 100644 index 00000000000..e441b1aa8ac --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb", + "spring": "\u6625", + "summer": "\u590f", + "winter": "\u51ac" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ko.json b/homeassistant/components/sensor/.translations/season.ko.json new file mode 100644 index 00000000000..f2bf0a7bae5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ko.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\uac00\uc744", + "spring": "\ubd04", + "summer": "\uc5ec\ub984", + "winter": "\uaca8\uc6b8" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.nl.json b/homeassistant/components/sensor/.translations/season.nl.json new file mode 100644 index 00000000000..6054a8e2be5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herfst", + "spring": "Lente", + "summer": "Zomer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.no.json b/homeassistant/components/sensor/.translations/season.no.json new file mode 100644 index 00000000000..9d520dae6a5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f8st", + "spring": "V\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pl.json b/homeassistant/components/sensor/.translations/season.pl.json new file mode 100644 index 00000000000..f5a7da57e7f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesie\u0144", + "spring": "Wiosna", + "summer": "Lato", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt.json b/homeassistant/components/sensor/.translations/season.pt.json new file mode 100644 index 00000000000..fde45ad6c8e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ro.json b/homeassistant/components/sensor/.translations/season.ro.json new file mode 100644 index 00000000000..04f90318290 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ro.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Toamn\u0103", + "spring": "Prim\u0103var\u0103", + "summer": "Var\u0103", + "winter": "Iarn\u0103" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sl.json b/homeassistant/components/sensor/.translations/season.sl.json new file mode 100644 index 00000000000..f715a3ec13a --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesen", + "spring": "Pomlad", + "summer": "Poletje", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sv.json b/homeassistant/components/sensor/.translations/season.sv.json new file mode 100644 index 00000000000..02332d76906 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f6st", + "spring": "V\u00e5r", + "summer": "Sommar", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.th.json b/homeassistant/components/sensor/.translations/season.th.json new file mode 100644 index 00000000000..09799730389 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.th.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e23\u0e48\u0e27\u0e07", + "spring": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e1c\u0e25\u0e34", + "summer": "\u0e24\u0e14\u0e39\u0e23\u0e49\u0e2d\u0e19", + "winter": "\u0e24\u0e14\u0e39\u0e2b\u0e19\u0e32\u0e27" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.vi.json b/homeassistant/components/sensor/.translations/season.vi.json new file mode 100644 index 00000000000..a3bb21dee27 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.vi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "M\u00f9a thu", + "spring": "M\u00f9a xu\u00e2n", + "summer": "M\u00f9a h\u00e8", + "winter": "M\u00f9a \u0111\u00f4ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hans.json b/homeassistant/components/sensor/.translations/season.zh-Hans.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hans.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hant.json b/homeassistant/components/sensor/.translations/season.zh-Hant.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 26bfd19e6fc..3208c7377df 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -14,14 +14,16 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = [ - 'remaining_range_fuel', - 'mileage', - ] +LENGTH_ATTRIBUTES = { + 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], + 'mileage': ['Mileage', 'mdi:speedometer'] +} -VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [ - 'remaining_fuel', -] +VALID_ATTRIBUTES = { + 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] +} + +VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -32,16 +34,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for sensor in VALID_ATTRIBUTES: - device = BMWConnectedDriveSensor(account, vehicle, sensor) + for key, value in sorted(VALID_ATTRIBUTES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) devices.append(device) - add_devices(devices) + add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str): + def __init__(self, account, vehicle, attribute: str, sensor_name, icon): """Constructor.""" self._vehicle = vehicle self._account = account @@ -49,6 +52,8 @@ class BMWConnectedDriveSensor(Entity): self._state = None self._unit_of_measurement = None self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name + self._icon = icon @property def should_poll(self) -> bool: @@ -60,6 +65,11 @@ class BMWConnectedDriveSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the sensor. @@ -74,9 +84,16 @@ class BMWConnectedDriveSensor(Entity): """Get the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return { + 'car': self._vehicle.modelName + } + def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self.entity_id) + _LOGGER.debug('Updating %s', self._vehicle.modelName) vehicle_state = self._vehicle.state self._state = getattr(vehicle_state, self._attribute) @@ -87,7 +104,9 @@ class BMWConnectedDriveSensor(Entity): else: self._unit_of_measurement = None - self.schedule_update_ha_state() + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) @asyncio.coroutine def async_added_to_hass(self): @@ -95,5 +114,4 @@ class BMWConnectedDriveSensor(Entity): Show latest data after startup. """ - self._account.add_update_listener(self.update) - yield from self.hass.async_add_job(self.update) + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index aecfca60bf1..a2d7315a314 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -89,6 +89,7 @@ class CrimeReportsSensor(Entity): return self._attributes def _incident_event(self, incident): + """Fire if an event occurs.""" data = { 'type': incident.get('type'), 'description': incident.get('friendly_description'), diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index e224feb7db7..7d535c5f1d9 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +27,9 @@ CONF_ATTRIBUTION = "Powered by Dark Sky" CONF_UNITS = 'units' CONF_UPDATE_INTERVAL = 'update_interval' CONF_FORECAST = 'forecast' +CONF_LANGUAGE = 'language' + +DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' @@ -51,7 +54,8 @@ SENSOR_TYPES = { 'mdi:weather-pouring', ['currently', 'minutely', 'hourly', 'daily']], 'precip_intensity': ['Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:weather-rainy', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:weather-rainy', ['currently', 'minutely', 'hourly', 'daily']], 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%', 'mdi:water-percent', @@ -97,7 +101,8 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:thermometer', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'uv_index': ['UV Index', UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, @@ -118,6 +123,16 @@ CONDITION_PICTURES = { 'partly-cloudy-night': '/static/images/darksky/weather-cloudy.svg', } +# Language Supported Codes +LANGUAGE_CODES = [ + 'ar', 'az', 'be', 'bg', 'bs', 'ca', + 'cs', 'da', 'de', 'el', 'en', 'es', + 'et', 'fi', 'fr', 'hr', 'hu', 'id', + 'is', 'it', 'ja', 'ka', 'kw', 'nb', + 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', + 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', + 'x-pig-latin', 'zh', 'zh-tw', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): @@ -125,6 +140,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), vol.Inclusive(CONF_LATITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', @@ -140,6 +157,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + language = config.get(CONF_LANGUAGE) if CONF_UNITS in config: units = config[CONF_UNITS] @@ -153,6 +171,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): latitude=latitude, longitude=longitude, units=units, + language=language, interval=config.get(CONF_UPDATE_INTERVAL)) forecast_data.update() forecast_data.update_currently() @@ -332,12 +351,14 @@ def convert_to_camel(data): class DarkSkyData(object): """Get the latest data from Darksky.""" - def __init__(self, api_key, latitude, longitude, units, interval): + def __init__(self, api_key, latitude, longitude, units, language, + interval): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.units = units + self.language = language self.data = None self.unit_system = None @@ -359,7 +380,8 @@ class DarkSkyData(object): try: self.data = forecastio.load_forecast( - self._api_key, self.latitude, self.longitude, units=self.units) + self._api_key, self.latitude, self.longitude, units=self.units, + lang=self.language) except (ConnectError, HTTPError, Timeout, ValueError) as error: _LOGGER.error("Unable to connect to Dark Sky. %s", error) self.data = None diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index b60df1c6ac9..081b304dc55 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -4,11 +4,10 @@ Support for deCONZ sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) from homeassistant.core import EventOrigin, callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -16,11 +15,12 @@ from homeassistant.util import slugify DEPENDENCIES = ['deconz'] +ATTR_CURRENT = 'current' ATTR_EVENT_ID = 'event_id' -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ sensors.""" if discovery_info is None: return @@ -29,8 +29,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_REMOTE: DeconzEvent(hass, sensor) @@ -48,8 +47,7 @@ class DeconzSensor(Entity): """Set up sensor and add update callback to get data from websocket.""" self._sensor = sensor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id @@ -109,9 +107,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self.unit_of_measurement == 'Watts': + attr[ATTR_CURRENT] = self._sensor.current + attr[ATTR_VOLTAGE] = self._sensor.voltage return attr @@ -125,8 +126,7 @@ class DeconzBattery(Entity): self._device_class = 'battery' self._unit_of_measurement = "%" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self._device.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._device.deconz_id diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 2b125155892..ec9b14883a9 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.21'] +REQUIREMENTS = ['schiene==0.22'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fb5fa2c1fba..06accb26eb6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1'] +REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index cde50699b29..3faf51a5f47 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -20,12 +20,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' +FILTER_NAME_TIME_SMA = 'time_simple_moving_average' FILTERS = Registry() CONF_FILTERS = 'filters' @@ -34,6 +36,9 @@ CONF_FILTER_WINDOW_SIZE = 'window_size' CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_TIME_SMA_TYPE = 'type' + +TIME_SMA_LAST = 'last' DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 @@ -44,24 +49,37 @@ NAME_TEMPLATE = "{} filter" ICON = 'mdi:chart-line-variant' FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), }) +# pylint: disable=redefined-builtin FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce(float), }) FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Optional(CONF_TIME_SMA_TYPE, + default=TIME_SMA_LAST): vol.In( + [TIME_SMA_LAST]), + + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, + cv.positive_timedelta) +}) + FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, }) @@ -72,6 +90,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILTERS): vol.All(cv.ensure_list, [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, + FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA)]) }) @@ -277,6 +296,49 @@ class LowPassFilter(Filter): return filtered +@FILTERS.register(FILTER_NAME_TIME_SMA) +class TimeSMAFilter(Filter): + """Simple Moving Average (SMA) Filter. + + The window_size is determined by time, and SMA is time weighted. + + Args: + variant (enum): type of argorithm used to connect discrete values + """ + + def __init__(self, window_size, precision, entity, type): + """Initialize Filter.""" + super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) + self._time_window = int(window_size.total_seconds()) + self.last_leak = None + self.queue = deque() + + def _leak(self, now): + """Remove timeouted elements.""" + while self.queue: + timestamp, _ = self.queue[0] + if timestamp + self._time_window <= now: + self.last_leak = self.queue.popleft() + else: + return + + def _filter_state(self, new_state): + now = int(dt_util.utcnow().timestamp()) + + self._leak(now) + self.queue.append((now, float(new_state))) + moving_sum = 0 + start = now - self._time_window + _, prev_val = self.last_leak or (0, float(new_state)) + + for timestamp, val in self.queue: + moving_sum += (timestamp-start)*prev_val + start, prev_val = timestamp, val + moving_sum += (now-start)*prev_val + + return moving_sum/self._time_window + + @FILTERS.register(FILTER_NAME_THROTTLE) class ThrottleFilter(Filter): """Throttle Filter. diff --git a/homeassistant/components/sensor/foobot.py b/homeassistant/components/sensor/foobot.py new file mode 100644 index 00000000000..8f65a335872 --- /dev/null +++ b/homeassistant/components/sensor/foobot.py @@ -0,0 +1,158 @@ +""" +Support for the Foobot indoor air quality monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.foobot/ +""" +import asyncio +import logging +from datetime import timedelta + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady +from homeassistant.const import ( + ATTR_TIME, ATTR_TEMPERATURE, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + + +REQUIREMENTS = ['foobot_async==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_HUMIDITY = 'humidity' +ATTR_PM2_5 = 'PM2.5' +ATTR_CARBON_DIOXIDE = 'CO2' +ATTR_VOLATILE_ORGANIC_COMPOUNDS = 'VOC' +ATTR_FOOBOT_INDEX = 'index' + +SENSOR_TYPES = {'time': [ATTR_TIME, 's'], + 'pm': [ATTR_PM2_5, 'µg/m3', 'mdi:cloud'], + 'tmp': [ATTR_TEMPERATURE, TEMP_CELSIUS, 'mdi:thermometer'], + 'hum': [ATTR_HUMIDITY, '%', 'mdi:water-percent'], + 'co2': [ATTR_CARBON_DIOXIDE, 'ppm', + 'mdi:periodic-table-co2'], + 'voc': [ATTR_VOLATILE_ORGANIC_COMPOUNDS, 'ppb', + 'mdi:cloud'], + 'allpollu': [ATTR_FOOBOT_INDEX, '%', 'mdi:percent']} + +SCAN_INTERVAL = timedelta(minutes=10) +PARALLEL_UPDATES = 1 + +TIMEOUT = 10 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the devices associated with the account.""" + from foobot_async import FoobotClient + + token = config.get(CONF_TOKEN) + username = config.get(CONF_USERNAME) + + client = FoobotClient(token, username, + async_get_clientsession(hass), + timeout=TIMEOUT) + dev = [] + try: + devices = await client.get_devices() + _LOGGER.debug("The following devices were found: %s", devices) + for device in devices: + foobot_data = FoobotData(client, device['uuid']) + for sensor_type in SENSOR_TYPES: + if sensor_type == 'time': + continue + foobot_sensor = FoobotSensor(foobot_data, device, sensor_type) + dev.append(foobot_sensor) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, FoobotClient.TooManyRequests, + FoobotClient.InternalError): + _LOGGER.exception('Failed to connect to foobot servers.') + raise PlatformNotReady + except FoobotClient.ClientError: + _LOGGER.error('Failed to fetch data from foobot servers.') + return + async_add_devices(dev, True) + + +class FoobotSensor(Entity): + """Implementation of a Foobot sensor.""" + + def __init__(self, data, device, sensor_type): + """Initialize the sensor.""" + self._uuid = device['uuid'] + self.foobot_data = data + self._name = 'Foobot {} {}'.format(device['name'], + SENSOR_TYPES[sensor_type][0]) + self.type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return the state of the device.""" + try: + data = self.foobot_data.data[self.type] + except(KeyError, TypeError): + data = None + return data + + @property + def unique_id(self): + """Return the unique id of this entity.""" + return "{}_{}".format(self._uuid, self.type) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + async def async_update(self): + """Get the latest data.""" + await self.foobot_data.async_update() + + +class FoobotData(Entity): + """Get data from Foobot API.""" + + def __init__(self, client, uuid): + """Initialize the data object.""" + self._client = client + self._uuid = uuid + self.data = {} + + @Throttle(SCAN_INTERVAL) + async def async_update(self): + """Get the data from Foobot API.""" + interval = SCAN_INTERVAL.total_seconds() + try: + response = await self._client.get_last_data(self._uuid, + interval, + interval + 1) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, self._client.TooManyRequests, + self._client.InternalError): + _LOGGER.debug("Couldn't fetch data") + return False + _LOGGER.debug("The data response is: %s", response) + self.data = {k: round(v, 1) for k, v in response[0].items()} + return True diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index b61b7abeae3..3b6f3ddc99d 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -42,6 +42,9 @@ SENSOR_TYPES = { 'process_thread': ['Thread', 'Count', 'mdi:memory'], 'process_sleeping': ['Sleeping', 'Count', 'mdi:memory'], 'cpu_temp': ['CPU Temp', TEMP_CELSIUS, 'mdi:thermometer'], + 'docker_active': ['Containers active', '', 'mdi:docker'], + 'docker_cpu_use': ['Containers CPU used', '%', 'mdi:docker'], + 'docker_memory_use': ['Containers RAM used', 'MiB', 'mdi:docker'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -155,6 +158,22 @@ class GlancesSensor(Entity): if sensor['label'] == 'CPU': self._state = sensor['value'] self._state = None + elif self.type == 'docker_active': + count = 0 + for container in value['docker']['containers']: + if container['Status'] == 'running': + count += 1 + self._state = count + elif self.type == 'docker_cpu_use': + use = 0.0 + for container in value['docker']['containers']: + use += container['cpu']['total'] + self._state = round(use, 1) + elif self.type == 'docker_memory_use': + use = 0.0 + for container in value['docker']['containers']: + use += container['memory']['usage'] + self._state = round(use / 1024**2, 1) class GlancesData(object): diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 936533422bb..350f1e2eb59 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -35,6 +35,7 @@ HM_STATE_HA_CAST = { HM_UNIT_HA_CAST = { 'HUMIDITY': '%', 'TEMPERATURE': '°C', + 'ACTUAL_TEMPERATURE': '°C', 'BRIGHTNESS': '#', 'POWER': 'W', 'CURRENT': 'mA', @@ -57,6 +58,7 @@ HM_ICON_HA_CAST = { 'WIND_SPEED': 'mdi:weather-windy', 'HUMIDITY': 'mdi:water-percent', 'TEMPERATURE': 'mdi:thermometer', + 'ACTUAL_TEMPERATURE': 'mdi:thermometer', 'LUX': 'mdi:weather-sunny', 'BRIGHTNESS': 'mdi:invert-colors', 'POWER': 'mdi:flash-red-eye', diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py new file mode 100644 index 00000000000..1a37aa1ad4e --- /dev/null +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -0,0 +1,259 @@ +""" +Support for HomematicIP sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.homematicip_cloud/ +""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, + ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) +from homeassistant.const import TEMP_CELSIUS, STATE_OK + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematicip_cloud'] + +ATTR_VALVE_STATE = 'valve_state' +ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE_OFFSET = 'temperature_offset' + +HMIP_UPTODATE = 'up_to_date' +HMIP_VALVE_DONE = 'adaption_done' +HMIP_SABOTAGE = 'sabotage' + +STATE_LOW_BATTERY = 'low_battery' +STATE_SABOTAGE = 'sabotage' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the HomematicIP sensors devices.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.device import ( + HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, + TemperatureHumiditySensorDisplay) + + homeid = discovery_info['homeid'] + home = hass.data[DOMAIN][homeid] + devices = [HomematicipAccesspoint(home)] + + for device in home.devices: + devices.append(HomematicipDeviceStatus(home, device)) + if isinstance(device, HeatingThermostat): + devices.append(HomematicipHeatingThermostat(home, device)) + if isinstance(device, TemperatureHumiditySensorWithoutDisplay): + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) + if isinstance(device, TemperatureHumiditySensorDisplay): + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) + + if home.devices: + add_devices(devices) + + +class HomematicipAccesspoint(Entity): + """Representation of an HomeMaticIP access point.""" + + def __init__(self, home): + """Initialize the access point sensor.""" + self._home = home + _LOGGER.debug('Setting up access point %s', home.label) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, EVENT_HOME_CHANGED, self._home_changed) + + @callback + def _home_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._home.id: + _LOGGER.debug('Event home %s', self._home.label) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the name of the access point device.""" + if self._home.label == '': + return 'Access Point Status' + return '{} Access Point Status'.format(self._home.label) + + @property + def icon(self): + """Return the icon of the access point device.""" + return 'mdi:access-point-network' + + @property + def state(self): + """Return the state of the access point.""" + return self._home.dutyCycle + + @property + def available(self): + """Device available.""" + return self._home.connected + + @property + def device_state_attributes(self): + """Return the state attributes of the access point.""" + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_HOME_ID: self._home.id, + } + + +class HomematicipDeviceStatus(HomematicipGenericDevice): + """Representation of an HomematicIP device status.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device) + _LOGGER.debug('Setting up sensor device status: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Status') + + @property + def icon(self): + """Return the icon of the status device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return 'mdi:alert' + elif self._device.lowBat: + return 'mdi:battery-outline' + elif self._device.updateState.lower() != HMIP_UPTODATE: + return 'mdi:refresh' + return 'mdi:check' + + @property + def state(self): + """Return the state of the generic device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return STATE_SABOTAGE + elif self._device.lowBat: + return STATE_LOW_BATTERY + elif self._device.updateState.lower() != HMIP_UPTODATE: + return self._device.updateState.lower() + return STATE_OK + + +class HomematicipHeatingThermostat(HomematicipGenericDevice): + """MomematicIP heating thermostat representation.""" + + def __init__(self, home, device): + """"Initialize heating thermostat.""" + super().__init__(home, device) + _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + + @property + def icon(self): + """Return the icon.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return 'mdi:alert' + return 'mdi:radiator' + + @property + def state(self): + """Return the state of the radiator valve.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return self._device.valveState.lower() + return round(self._device.valvePosition*100) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_VALVE_STATE: self._device.valveState.lower(), + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue + } + + +class HomematicipSensorHumidity(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, home, device): + """"Initialize the thermometer device.""" + super().__init__(home, device) + _LOGGER.debug('Setting up humidity device: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Humidity') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:water' + + @property + def state(self): + """Return the state.""" + return self._device.humidity + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } + + +class HomematicipSensorThermometer(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, home, device): + """"Initialize the thermometer device.""" + super().__init__(home, device) + _LOGGER.debug('Setting up thermometer device: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Temperature') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:thermometer' + + @property + def state(self): + """Return the state.""" + return self._device.actualTemperature + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py index 3e0a5af283f..034f0be49f6 100644 --- a/homeassistant/components/sensor/lacrosse.py +++ b/homeassistant/components/sensor/lacrosse.py @@ -157,7 +157,7 @@ class LaCrosseSensor(Entity): self._expiration_trigger = async_track_point_in_utc_time( self.hass, self.value_is_expired, expiration_at) - self._temperature = round(lacrosse_sensor.temperature * 2) / 2 + self._temperature = lacrosse_sensor.temperature self._humidity = lacrosse_sensor.humidity self._low_battery = lacrosse_sensor.low_battery self._new_battery = lacrosse_sensor.new_battery diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index b19f5721e4f..d191b9a22e8 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -36,6 +36,7 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -59,6 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_UNIT_OF_MEASUREMENT), config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), + config.get(CONF_ICON), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_AVAILABILITY_TOPIC), @@ -71,7 +73,7 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, value_template, + force_update, expire_after, icon, value_template, json_attributes, availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -85,6 +87,7 @@ class MqttSensor(MqttAvailability, Entity): self._force_update = force_update self._template = value_template self._expire_after = expire_after + self._icon = icon self._expiration_trigger = None self._json_attributes = set(json_attributes) self._attributes = None @@ -170,3 +173,8 @@ class MqttSensor(MqttAvailability, Entity): def device_state_attributes(self): """Return the state attributes.""" return self._attributes + + @property + def icon(self): + """Return the icon.""" + return self._icon diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index a8daf212e57..3876b260dfc 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -8,6 +8,31 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +SENSORS = { + 'V_TEMP': [None, 'mdi:thermometer'], + 'V_HUM': ['%', 'mdi:water-percent'], + 'V_DIMMER': ['%', 'mdi:percent'], + 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_DIRECTION': ['°', 'mdi:compass'], + 'V_WEIGHT': ['kg', 'mdi:weight-kilogram'], + 'V_DISTANCE': ['m', 'mdi:ruler'], + 'V_IMPEDANCE': ['ohm', None], + 'V_WATT': ['W', None], + 'V_KWH': ['kWh', None], + 'V_FLOW': ['m', None], + 'V_VOLUME': ['m³', None], + 'V_VOLTAGE': ['V', 'mdi:flash'], + 'V_CURRENT': ['A', 'mdi:flash-auto'], + 'V_PERCENTAGE': ['%', 'mdi:percent'], + 'V_LEVEL': { + 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], + 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'V_ORP': ['mV', None], + 'V_EC': ['μS/cm', None], + 'V_VAR': ['var', None], + 'V_VA': ['VA', None], +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" @@ -32,45 +57,29 @@ class MySensorsSensor(mysensors.MySensorsEntity): """Return the state of the device.""" return self._values.get(self.value_type) + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + _, icon = self._get_sensor_type() + return icon + @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" + set_req = self.gateway.const.SetReq + if (float(self.gateway.protocol_version) >= 1.5 and + set_req.V_UNIT_PREFIX in self._values): + return self._values[set_req.V_UNIT_PREFIX] + unit, _ = self._get_sensor_type() + return unit + + def _get_sensor_type(self): + """Return list with unit and icon of sensor type.""" pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - unit_map = { - set_req.V_TEMP: (TEMP_CELSIUS - if self.gateway.metric else TEMP_FAHRENHEIT), - set_req.V_HUM: '%', - set_req.V_DIMMER: '%', - set_req.V_LIGHT_LEVEL: '%', - set_req.V_DIRECTION: '°', - set_req.V_WEIGHT: 'kg', - set_req.V_DISTANCE: 'm', - set_req.V_IMPEDANCE: 'ohm', - set_req.V_WATT: 'W', - set_req.V_KWH: 'kWh', - set_req.V_FLOW: 'm', - set_req.V_VOLUME: 'm³', - set_req.V_VOLTAGE: 'V', - set_req.V_CURRENT: 'A', - } - if float(self.gateway.protocol_version) >= 1.5: - if set_req.V_UNIT_PREFIX in self._values: - return self._values[ - set_req.V_UNIT_PREFIX] - unit_map.update({ - set_req.V_PERCENTAGE: '%', - set_req.V_LEVEL: { - pres.S_SOUND: 'dB', pres.S_VIBRATION: 'Hz', - pres.S_LIGHT_LEVEL: 'lux'}}) - if float(self.gateway.protocol_version) >= 2.0: - unit_map.update({ - set_req.V_ORP: 'mV', - set_req.V_EC: 'μS/cm', - set_req.V_VAR: 'var', - set_req.V_VA: 'VA', - }) - unit = unit_map.get(self.value_type) - if isinstance(unit, dict): - unit = unit.get(self.child_type) - return unit + SENSORS[set_req.V_TEMP.name][0] = ( + TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) + sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + if isinstance(sensor_type, dict): + sensor_type = sensor_type.get(pres(self.child_type).name) + return sensor_type diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 87af51d2bbd..505983cb3a7 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -115,9 +115,41 @@ class PlexSensor(Entity): sessions = self._server.sessions() now_playing = [] for sess in sessions: - user = sess.usernames[0] if sess.usernames is not None else "" - title = sess.title if sess.title is not None else "" - year = sess.year if sess.year is not None else "" - now_playing.append((user, "{0} ({1})".format(title, year))) + user = sess.usernames[0] + device = sess.players[0].title + now_playing_user = "{0} - {1}".format(user, device) + now_playing_title = "" + + if sess.TYPE == 'episode': + # example: + # "Supernatural (2005) - S01 · E13 - Route 666" + season_title = sess.grandparentTitle + if sess.show().year is not None: + season_title += " ({0})".format(sess.show().year) + season_episode = "S{0}".format(sess.parentIndex) + if sess.index is not None: + season_episode += " · E{1}".format(sess.index) + episode_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(season_title, + season_episode, + episode_title) + elif sess.TYPE == 'track': + # example: + # "Billy Talent - Afraid of Heights - Afraid of Heights" + track_artist = sess.grandparentTitle + track_album = sess.parentTitle + track_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(track_artist, + track_album, + track_title) + else: + # example: + # "picture_of_last_summer_camp (2015)" + # "The Incredible Hulk (2008)" + now_playing_title = sess.title + if sess.year is not None: + now_playing_title += " ({0})".format(sess.year) + + now_playing.append((now_playing_user, now_playing_title)) self._state = len(sessions) self._now_playing = now_playing diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 51595d19b1a..c59798d16d7 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -21,17 +21,17 @@ SENSOR_TYPES = { 'active_power': ['Active Power', 'mdi:power-plug', 'local', 'W', 'active_power'], 'current': - ['Current', 'mdi:gauge', 'local', 'Amps', 'current'], + ['Current', 'mdi:gauge', 'local', 'A', 'current'], 'voltage': ['Voltage', 'mdi:gauge', 'local', 'V', 'voltage'], 'active_cosfi': ['Power Factor', 'mdi:gauge', 'local', '%', 'active_cosfi'], 'alwayson_today': - ['Always On Today', 'mdi:gauge', 'remote', 'kW', 'alwaysOn'], + ['Always On Today', 'mdi:gauge', 'remote', 'kWh', 'alwaysOn'], 'solar_today': - ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kW', 'solar'], + ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kW', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] } SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py index 169bcc5f867..08177c9a7b9 100644 --- a/homeassistant/components/sensor/spotcrime.py +++ b/homeassistant/components/sensor/spotcrime.py @@ -12,14 +12,15 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_RADIUS) +from homeassistant.const import (CONF_API_KEY, CONF_INCLUDE, CONF_EXCLUDE, + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_RADIUS) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['spotcrime==1.0.2'] +REQUIREMENTS = ['spotcrime==1.0.3'] _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int, @@ -49,28 +51,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config[CONF_NAME] radius = config[CONF_RADIUS] + api_key = config[CONF_API_KEY] days = config.get(CONF_DAYS) include = config.get(CONF_INCLUDE) exclude = config.get(CONF_EXCLUDE) add_devices([SpotCrimeSensor( name, latitude, longitude, radius, include, - exclude, days)], True) + exclude, api_key, days)], True) class SpotCrimeSensor(Entity): """Representation of a Spot Crime Sensor.""" def __init__(self, name, latitude, longitude, radius, - include, exclude, days): + include, exclude, api_key, days): """Initialize the Spot Crime sensor.""" import spotcrime self._name = name self._include = include self._exclude = exclude + self.api_key = api_key self.days = days self._spotcrime = spotcrime.SpotCrime( - (latitude, longitude), radius, None, None, self.days) + (latitude, longitude), radius, self._include, + self._exclude, self.api_key, self.days) self._attributes = None self._state = None self._previous_incidents = set() diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 5d5d61ff822..af9fa233d40 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] CONF_QUERIES = 'queries' CONF_QUERY = 'query' diff --git a/homeassistant/components/sensor/syncthru.py b/homeassistant/components/sensor/syncthru.py new file mode 100644 index 00000000000..a24482bda01 --- /dev/null +++ b/homeassistant/components/sensor/syncthru.py @@ -0,0 +1,233 @@ +""" +Support for Samsung Printers with SyncThru web interface. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.syncthru/ +""" + +import logging +import voluptuous as vol + +from homeassistant.const import ( + CONF_RESOURCE, CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA + +REQUIREMENTS = ['pysyncthru==0.3.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Samsung Printer' +DEFAULT_MONITORED_CONDITIONS = [ + 'toner_black', + 'toner_cyan', + 'toner_magenta', + 'toner_yellow', + 'drum_black', + 'drum_cyan', + 'drum_magenta', + 'drum_yellow', + 'tray_1', + 'tray_2', + 'tray_3', + 'tray_4', + 'tray_5', + 'output_tray_0', + 'output_tray_1', + 'output_tray_2', + 'output_tray_3', + 'output_tray_4', + 'output_tray_5', +] +COLORS = [ + 'black', + 'cyan', + 'magenta', + 'yellow' +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional( + CONF_NAME, + default=DEFAULT_NAME + ): cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=DEFAULT_MONITORED_CONDITIONS + ): vol.All(cv.ensure_list, [vol.In(DEFAULT_MONITORED_CONDITIONS)]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the SyncThru component.""" + from pysyncthru import SyncThru, test_syncthru + + if discovery_info is not None: + host = discovery_info.get(CONF_HOST) + name = discovery_info.get(CONF_NAME, DEFAULT_NAME) + _LOGGER.debug("Discovered a new Samsung Printer: %s", discovery_info) + # Test if the discovered device actually is a syncthru printer + if not test_syncthru(host): + _LOGGER.error("No SyncThru Printer found at %s", host) + return + monitored = DEFAULT_MONITORED_CONDITIONS + else: + host = config.get(CONF_RESOURCE) + name = config.get(CONF_NAME) + monitored = config.get(CONF_MONITORED_CONDITIONS) + + # Main device, always added + try: + printer = SyncThru(host) + except TypeError: + # if an exception is thrown, printer cannot be set up + return + + printer.update() + devices = [SyncThruMainSensor(printer, name)] + + for key in printer.toner_status(filter_supported=True): + if 'toner_{}'.format(key) in monitored: + devices.append(SyncThruTonerSensor(printer, name, key)) + for key in printer.drum_status(filter_supported=True): + if 'drum_{}'.format(key) in monitored: + devices.append(SyncThruDrumSensor(printer, name, key)) + for key in printer.input_tray_status(filter_supported=True): + if 'tray_{}'.format(key) in monitored: + devices.append(SyncThruInputTraySensor(printer, name, key)) + for key in printer.output_tray_status(): + if 'output_tray_{}'.format(key) in monitored: + devices.append(SyncThruOutputTraySensor(printer, name, key)) + + add_devices(devices, True) + + +class SyncThruSensor(Entity): + """Implementation of an abstract Samsung Printer sensor platform.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + self.syncthru = syncthru + self._attributes = {} + self._state = None + self._name = name + self._icon = 'mdi:printer' + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measuremnt.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._attributes + + +class SyncThruMainSensor(SyncThruSensor): + """Implementation of the main sensor, monitoring the general state.""" + + def update(self): + """Get the latest data from SyncThru and update the state.""" + self.syncthru.update() + self._state = self.syncthru.device_status() + + +class SyncThruTonerSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Toner {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.toner_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruDrumSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Drum {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.drum_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruInputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.input_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('newError') + if self._state == '': + self._state = 'Ready' + + +class SyncThruOutputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Output Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.output_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('status') + if self._state == '': + self._state = 'Ready' diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 79d5c261b88..2f970796fe1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -42,8 +42,8 @@ SENSOR_TYPES = { 'process': ['Process', ' ', 'mdi:memory'], 'processor_use': ['Processor use', '%', 'mdi:memory'], 'since_last_boot': ['Since last boot', '', 'mdi:clock'], - 'swap_free': ['Swap free', 'GiB', 'mdi:harddisk'], - 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap free', 'MiB', 'mdi:harddisk'], + 'swap_use': ['Swap use', 'MiB', 'mdi:harddisk'], 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'], } @@ -135,9 +135,9 @@ class SystemMonitorSensor(Entity): elif self.type == 'swap_use_percent': self._state = psutil.swap_memory().percent elif self.type == 'swap_use': - self._state = round(psutil.swap_memory().used / 1024**3, 1) + self._state = round(psutil.swap_memory().used / 1024**2, 1) elif self.type == 'swap_free': - self._state = round(psutil.swap_memory().free / 1024**3, 1) + self._state = round(psutil.swap_memory().free / 1024**2, 1) elif self.type == 'processor_use': self._state = round(psutil.cpu_percent(interval=None)) elif self.type == 'process': diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 582bc3a0150..1cd43262513 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -14,7 +14,8 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID, - CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE) + CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE, + MATCH_ALL) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -48,22 +49,32 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get( CONF_ENTITY_PICTURE_TEMPLATE) - entity_ids = (device_config.get(ATTR_ENTITY_ID) or - state_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) - state_template.hass = hass + entity_ids = set() + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - if icon_template is not None: - icon_template.hass = hass + for template in (state_template, icon_template, + entity_picture_template, friendly_name_template): + if template is None: + continue + template.hass = hass - if entity_picture_template is not None: - entity_picture_template.hass = hass + if entity_ids == MATCH_ALL or manual_entity_ids is not None: + continue - if friendly_name_template is not None: - friendly_name_template.hass = hass + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + else: + entity_ids |= set(template_entity_ids) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) sensors.append( SensorTemplate( @@ -166,10 +177,10 @@ class SensorTemplate(Entity): # Common during HA startup - so just a warning _LOGGER.warning('Could not render template %s,' ' the state is unknown.', self._name) - return - self._state = None - _LOGGER.error('Could not render template %s: %s', self._name, ex) - + else: + self._state = None + _LOGGER.error('Could not render template %s: %s', self._name, + ex) for property_name, template in ( ('_icon', self._icon_template), ('_entity_picture', self._entity_picture_template), @@ -187,7 +198,7 @@ class SensorTemplate(Entity): _LOGGER.warning('Could not render %s template %s,' ' the state is unknown.', friendly_property_name, self._name) - return + continue try: setattr(self, property_name, diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index a5f490c8d51..aaaa8366909 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -REQUIREMENTS = ['pyTibber==0.3.2'] +REQUIREMENTS = ['pyTibber==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,8 @@ class TibberSensor(Entity): self._state = None self._device_state_attributes = {} self._unit_of_measurement = self._tibber_home.price_unit - self._name = 'Electricity price {}'.format(tibber_home.address1) + self._name = 'Electricity price {}'.format(tibber_home.info['viewer'] + ['home']['appNickname']) async def async_update(self): """Get the latest data and updates the states.""" @@ -72,14 +73,25 @@ class TibberSensor(Entity): return def _find_current_price(): + state = None + max_price = None + min_price = None for key, price_total in self._tibber_home.price_total.items(): price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 if time_diff >= 0 and time_diff < 60: - self._state = round(price_total, 2) + state = price_total self._last_updated = key - return True - return False + if now.date() == price_time.date(): + if max_price is None or price_total > max_price: + max_price = price_total + if min_price is None or price_total < min_price: + min_price = price_total + self._state = state + self._device_state_attributes['max_price'] = max_price + self._device_state_attributes['min_price'] = min_price + return state is not None if _find_current_price(): return diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py new file mode 100644 index 00000000000..fba16c27c7e --- /dev/null +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -0,0 +1,124 @@ +""" +Weather information for air and road temperature, provided by Trafikverket. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.trafikverket_weatherstation/ +""" +import json +import logging +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_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by Trafikverket API" + +CONF_STATION = 'station' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + vol.Required(CONF_TYPE): vol.In(['air', 'road']), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + sensor_name = config.get(CONF_NAME) + sensor_api = config.get(CONF_API_KEY) + sensor_station = config.get(CONF_STATION) + sensor_type = config.get(CONF_TYPE) + + add_devices([TrafikverketWeatherStation( + sensor_name, sensor_api, sensor_station, sensor_type)], True) + + +class TrafikverketWeatherStation(Entity): + """Representation of a Sensor.""" + + def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): + """Initialize the sensor.""" + self._name = sensor_name + self._api = sensor_api + self._station = sensor_station + self._type = sensor_type + self._state = None + self._attributes = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + + @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 unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' + + if self._type == 'road': + air_vs_road = 'Road' + else: + air_vs_road = 'Air' + + xml = """ + + + + + + + Measurement.""" + air_vs_road + """.Temp + + """ + + # Testing JSON post request. + try: + post = requests.post(url, data=xml.encode('utf-8'), timeout=5) + except requests.exceptions.RequestException as err: + _LOGGER.error("Please check network connection: %s", err) + return + + # Checking JSON respons. + try: + data = json.loads(post.text) + result = data["RESPONSE"]["RESULT"][0] + final = result["WeatherStation"][0]["Measurement"] + except KeyError: + _LOGGER.error("Incorrect weather station or API key.") + return + + # air_vs_road contains "Air" or "Road" depending on user input. + self._state = final[air_vs_road]["Temp"] diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 983c589c98b..8cd084e1b71 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -30,6 +30,7 @@ CONF_DELAY = 'delay' CONF_DEPARTURES = 'departures' CONF_FROM = 'from' CONF_HEADING = 'heading' +CONF_LINES = 'lines' CONF_KEY = 'key' CONF_SECRET = 'secret' @@ -46,6 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FROM): cv.string, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, vol.Optional(CONF_HEADING): cv.string, + vol.Optional(CONF_LINES, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_NAME): cv.string}] }) @@ -61,14 +64,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): VasttrafikDepartureSensor( vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), departure.get(CONF_HEADING), - departure.get(CONF_DELAY))) + departure.get(CONF_LINES), departure.get(CONF_DELAY))) add_devices(sensors, True) class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, delay): + def __init__(self, vasttrafik, planner, name, departure, heading, + lines, delay): """Initialize the sensor.""" self._vasttrafik = vasttrafik self._planner = planner @@ -76,6 +80,7 @@ class VasttrafikDepartureSensor(Entity): self._departure = planner.location_name(departure)[0] self._heading = (planner.location_name(heading)[0] if heading else None) + self._lines = lines if lines else None self._delay = timedelta(minutes=delay) self._departureboard = None @@ -94,15 +99,18 @@ class VasttrafikDepartureSensor(Entity): """Return the state attributes.""" if not self._departureboard: return - departure = self._departureboard[0] - params = { - ATTR_ACCESSIBILITY: departure.get('accessibility', None), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DIRECTION: departure.get('direction', None), - ATTR_LINE: departure.get('sname', None), - ATTR_TRACK: departure.get('track', None), - } - return {k: v for k, v in params.items() if v} + + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + params = { + ATTR_ACCESSIBILITY: departure.get('accessibility'), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DIRECTION: departure.get('direction'), + ATTR_LINE: departure.get('sname'), + ATTR_TRACK: departure.get('track'), + } + return {k: v for k, v in params.items() if v} @property def state(self): @@ -113,9 +121,18 @@ class VasttrafikDepartureSensor(Entity): self._departure['name'], self._heading['name'] if self._heading else 'ANY') return - if 'rtTime' in self._departureboard[0]: - return self._departureboard[0]['rtTime'] - return self._departureboard[0]['time'] + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + if 'rtTime' in self._departureboard[0]: + return self._departureboard[0]['rtTime'] + return self._departureboard[0]['time'] + # No departures of given lines found + _LOGGER.debug( + "No departures from %s heading %s on line(s) %s", + self._departure['name'], + self._heading['name'] if self._heading else 'ANY', + ', '.join((str(line) for line in self._lines))) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 0375bb1344c..7938b17e4d6 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -14,11 +14,12 @@ import async_timeout import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -617,6 +618,8 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] +DEFAULT_ENTITY_NAMESPACE = 'pws' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -627,22 +630,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_ENTITY_NAMESPACE, + default=DEFAULT_ENTITY_NAMESPACE): cv.string, }) +# Stores a list of entity ids we added in order to support multiple stations +# at once. +ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' + @asyncio.coroutine def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" + hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + namespace = config.get(CONF_ENTITY_NAMESPACE) rest = WUndergroundData( hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), config.get(CONF_LANG), latitude, longitude) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable)) + sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) yield from rest.async_update() if not rest.data: @@ -654,7 +666,8 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistantType, rest, condition): + def __init__(self, hass: HomeAssistantType, rest, condition, + namespace: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -666,8 +679,12 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) + current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) + current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "pws_" + condition, hass=hass) + ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), + current_ids=current_ids) + hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py new file mode 100644 index 00000000000..066dc384007 --- /dev/null +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -0,0 +1,152 @@ +""" +Support for Xiaomi Mi Air Quality Monitor (PM2.5). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/sensor.xiaomi_miio/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN) +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Xiaomi Miio Sensor' +DATA_KEY = 'sensor.xiaomi_miio' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] + +ATTR_POWER = 'power' +ATTR_CHARGING = 'charging' +ATTR_BATTERY_LEVEL = 'battery_level' +ATTR_TIME_STATE = 'time_state' +ATTR_MODEL = 'model' + +SUCCESS = ['ok'] + + +# pylint: disable=unused-argument +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the sensor from config.""" + from miio import AirQualityMonitor, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + air_quality_monitor = AirQualityMonitor(host, token) + device_info = air_quality_monitor.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + device = XiaomiAirQualityMonitor( + name, air_quality_monitor, model, unique_id) + except DeviceException: + raise PlatformNotReady + + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) + + +class XiaomiAirQualityMonitor(Entity): + """Representation of a Xiaomi Air Quality Monitor.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the entity.""" + self._name = name + self._device = device + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:cloud' + self._unit_of_measurement = 'AQI' + self._available = None + self._state = None + self._state_attrs = { + ATTR_POWER: None, + ATTR_BATTERY_LEVEL: None, + ATTR_CHARGING: None, + ATTR_TIME_STATE: None, + ATTR_MODEL: self._model, + } + + @property + def should_poll(self): + """Poll the miio device.""" + return True + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._state_attrs + + async def async_update(self): + """Fetch state from the miio device.""" + from miio import DeviceException + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.aqi + self._state_attrs.update({ + ATTR_POWER: state.power, + ATTR_CHARGING: state.usb_power, + ATTR_BATTERY_LEVEL: state.battery, + ATTR_TIME_STATE: state.time_state, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 10544b3ef53..9742bc25c63 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -189,12 +189,7 @@ class SpcWebGateway: def start_listener(self, async_callback, *args): """Start the websocket listener.""" - try: - from asyncio import ensure_future - except ImportError: - from asyncio import async as ensure_future - - ensure_future(self._ws_listen(async_callback, *args)) + asyncio.ensure_future(self._ws_listen(async_callback, *args)) def _build_url(self, resource): return urljoin(self._api_url, "spc/{}".format(resource)) diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py index 4ab8eea6ec4..9886b3a586d 100644 --- a/homeassistant/components/switch/doorbird.py +++ b/homeassistant/components/switch/doorbird.py @@ -22,6 +22,14 @@ SWITCHES = { }, "time": datetime.timedelta(seconds=3) }, + "open_door_2": { + "name": "Open Door 2", + "icon": { + True: "lock-open", + False: "lock" + }, + "time": datetime.timedelta(seconds=3) + }, "light_on": { "name": "Light On", "icon": { @@ -80,6 +88,8 @@ class DoorBirdSwitch(SwitchDevice): """Power the relay.""" if self._switch == "open_door": self._state = self._device.open_door() + elif self._switch == "open_door_2": + self._state = self._device.open_door(2) elif self._switch == "light_on": self._state = self._device.turn_light_on() diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 50b5ba93b85..49eb5d32110 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -83,14 +83,13 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update edimax switch.""" try: - self._now_power = float(self.smartplug.now_power) / 1000000.0 - except (TypeError, ValueError): + self._now_power = float(self.smartplug.now_power) + except ValueError: self._now_power = None try: - self._now_energy_day = (float(self.smartplug.now_energy_day) / - 1000.0) - except (TypeError, ValueError): + self._now_energy_day = float(self.smartplug.now_energy_day) + except ValueError: self._now_energy_day = None self._state = self.smartplug.state == 'ON' diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index acc0c3ac423..e0bfdeee030 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy, + color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired) from homeassistant.util.dt import now as dt_now @@ -234,7 +234,7 @@ class FluxSwitch(SwitchDevice): else: temp = self._sunset_colortemp + temp_offset rgb = color_temperature_to_rgb(temp) - x_val, y_val, b_val = color_RGB_to_xy(*rgb) + x_val, y_val, b_val = color_RGB_to_xy_brightness(*rgb) brightness = self._brightness if self._brightness else b_val if self._disable_brightness_adjust: brightness = None diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index a4aea1ded9f..f3bd0bef012 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON) + CONF_PAYLOAD_ON, CONF_ICON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -32,6 +32,7 @@ DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, 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, @@ -50,6 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices([MqttSwitch( config.get(CONF_NAME), + config.get(CONF_ICON), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_AVAILABILITY_TOPIC), @@ -67,7 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttSwitch(MqttAvailability, SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, name, state_topic, command_topic, availability_topic, + def __init__(self, name, icon, + state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" @@ -75,6 +78,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): payload_not_available) self._state = False self._name = name + self._icon = icon self._state_topic = state_topic self._command_topic = command_topic self._qos = qos @@ -130,6 +134,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def icon(self): + """Return the icon.""" + return self._icon + @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on. diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 51184859fc6..b4a1dcde3e6 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -72,6 +72,12 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if unable to access real state of entity.""" return self.gateway.optimistic + @property + def current_power_w(self): + """Return the current power usage in W.""" + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_WATT) + @property def is_on(self): """Return True if switch is on.""" diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index f52b197d432..46b1237f57c 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -30,3 +30,34 @@ mysensors_send_ir_code: V_IR_SEND: description: IR code to send. example: '0xC284' + +xiaomi_miio_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 +xiaomi_miio_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' diff --git a/homeassistant/components/switch/vesync.py b/homeassistant/components/switch/vesync.py new file mode 100644 index 00000000000..fbc73545e19 --- /dev/null +++ b/homeassistant/components/switch/vesync.py @@ -0,0 +1,104 @@ +""" +Support for Etekcity VeSync switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.vesync/ +""" +import logging +import voluptuous as vol +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['pyvesync==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the VeSync switch platform.""" + from pyvesync.vesync import VeSync + + switches = [] + + manager = VeSync(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) + + if not manager.login(): + _LOGGER.error("Unable to login to VeSync") + return + + manager.update() + + if manager.devices is not None and manager.devices: + if len(manager.devices) == 1: + count_string = 'switch' + else: + count_string = 'switches' + + _LOGGER.info("Discovered %d VeSync %s", + len(manager.devices), count_string) + + for switch in manager.devices: + switches.append(VeSyncSwitchHA(switch)) + _LOGGER.info("Added a VeSync switch named '%s'", + switch.device_name) + else: + _LOGGER.info("No VeSync devices found") + + add_devices(switches) + + +class VeSyncSwitchHA(SwitchDevice): + """Representation of a VeSync switch.""" + + def __init__(self, plug): + """Initialize the VeSync switch device.""" + self.smartplug = plug + + @property + def unique_id(self): + """Return the ID of this switch.""" + return self.smartplug.cid + + @property + def name(self): + """Return the name of the switch.""" + return self.smartplug.device_name + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self.smartplug.get_power() + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + return self.smartplug.get_kwh_today() + + @property + def available(self) -> bool: + """Return True if switch is available.""" + return self.smartplug.connection_status == "online" + + @property + def is_on(self): + """Return True if switch is on.""" + return self.smartplug.device_status == "on" + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self.smartplug.turn_on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self.smartplug.turn_off() + + def update(self): + """Handle data changes for node values.""" + self.smartplug.update() diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index ae4329a42a1..6110b6dc469 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -11,15 +11,19 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, + DOMAIN, ) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, + ATTR_ENTITY_ID, ) from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Miio Switch' +DATA_KEY = 'switch.xiaomi_miio' CONF_MODEL = 'model' +MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -33,20 +37,69 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' ATTR_LOAD_POWER = 'load_power' ATTR_MODEL = 'model' +ATTR_MODE = 'mode' +ATTR_POWER_MODE = 'power_mode' +ATTR_WIFI_LED = 'wifi_led' +ATTR_POWER_PRICE = 'power_price' +ATTR_PRICE = 'price' + SUCCESS = ['ok'] +SUPPORT_SET_POWER_MODE = 1 +SUPPORT_SET_WIFI_LED = 2 +SUPPORT_SET_POWER_PRICE = 4 + +ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0 + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE | + SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on' +SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off' +SERVICE_SET_POWER_MODE = 'xiaomi_miio_set_power_mode' +SERVICE_SET_POWER_PRICE = 'xiaomi_miio_set_power_price' + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): vol.All(vol.In(['green', 'normal'])), +}) + +SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_PRICE): vol.All(vol.Coerce(float), vol.Range(min=0)) +}) + +SERVICE_TO_METHOD = { + SERVICE_SET_WIFI_LED_ON: {'method': 'async_set_wifi_led_on'}, + SERVICE_SET_WIFI_LED_OFF: {'method': 'async_set_wifi_led_off'}, + SERVICE_SET_POWER_MODE: { + 'method': 'async_set_power_mode', + 'schema': SERVICE_SCHEMA_POWER_MODE}, + SERVICE_SET_POWER_PRICE: { + 'method': 'async_set_power_price', + 'schema': SERVICE_SCHEMA_POWER_PRICE}, +} + # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the switch from config.""" from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -56,12 +109,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) devices = [] + unique_id = None if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -77,21 +132,24 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # A switch device per channel will be created. for channel_usb in [True, False]: device = ChuangMiPlugV1Switch( - name, plug, model, channel_usb) + name, plug, model, unique_id, channel_usb) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip plug = PowerStrip(host, token) - device = XiaomiPowerStripSwitch(name, plug, model) + device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: from miio import Plug plug = Plug(host, token) - device = XiaomiPlugGenericSwitch(name, plug, model) + device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' @@ -101,22 +159,52 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(devices, update_before_add=True) + async def async_service_handler(service): + """Map services to methods on XiaomiPlugGenericSwitch.""" + method = SERVICE_TO_METHOD.get(service.service) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [device for device in hass.data[DATA_KEY].values() if + device.entity_id in entity_ids] + else: + devices = hass.data[DATA_KEY].values() + + update_tasks = [] + for device in devices: + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) + update_tasks.append(device.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks, loop=hass.loop) + + for plug_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[plug_service].get('schema', SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, plug_service, async_service_handler, schema=schema) + class XiaomiPlugGenericSwitch(SwitchDevice): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" self._name = name - self._icon = 'mdi:power-socket' - self._model = model - self._plug = plug + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:power-socket' + self._available = False self._state = None self._state_attrs = { ATTR_TEMPERATURE: None, ATTR_MODEL: self._model, } + self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC self._skip_update = False @property @@ -124,6 +212,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Poll the plug.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -137,7 +230,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -149,12 +242,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Return true if switch is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from plug: %s", result) @@ -162,30 +254,28 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the plug on.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the plug off.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug off failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -195,34 +285,75 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_wifi_led_on(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return -class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): + await self._try_command( + "Turning the wifi led on failed.", + self._plug.set_wifi_led, True) + + async def async_set_wifi_led_off(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return + + await self._try_command( + "Turning the wifi led off failed.", + self._plug.set_wifi_led, False) + + async def async_set_power_price(self, price: int): + """Set the power price.""" + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0: + return + + await self._try_command( + "Setting the power price of the power strip failed.", + self._plug.set_power_price, price) + + +class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): """Representation of a Xiaomi Power Strip.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) - self._state_attrs = { - ATTR_TEMPERATURE: None, + if self._model == MODEL_POWER_STRIP_V2: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 + else: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 + + self._state_attrs.update({ ATTR_LOAD_POWER: None, - ATTR_MODEL: self._model, - } + }) - @asyncio.coroutine - def async_update(self): + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1: + self._state_attrs[ATTR_POWER_MODE] = None + + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1: + self._state_attrs[ATTR_WIFI_LED] = None + + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1: + self._state_attrs[ATTR_POWER_PRICE] = None + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -232,60 +363,84 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature, - ATTR_LOAD_POWER: state.load_power + ATTR_LOAD_POWER: state.load_power, }) + if self._additional_supported_features & \ + SUPPORT_SET_POWER_MODE == 1 and state.mode: + self._state_attrs[ATTR_POWER_MODE] = state.mode.value + + if self._additional_supported_features & \ + SUPPORT_SET_WIFI_LED == 1 and state.wifi_led: + self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + + if self._additional_supported_features & \ + SUPPORT_SET_POWER_PRICE == 1 and state.power_price: + self._state_attrs[ATTR_POWER_PRICE] = state.power_price + except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_power_mode(self, mode: str): + """Set the power mode.""" + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0: + return -class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): + from miio.powerstrip import PowerMode + + await self._try_command( + "Setting the power mode of the power strip failed.", + self._plug.set_power_mode, PowerMode(mode)) + + +class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): """Representation of a Chuang Mi Plug V1.""" - def __init__(self, name, plug, model, channel_usb): + def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" name = '{} USB'.format(name) if channel_usb else name - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + if unique_id is not None and channel_usb: + unique_id = "{}-{}".format(unique_id, 'usb') + + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) self._channel_usb = channel_usb - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn a channel on.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_on) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn a channel off.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_off) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -295,9 +450,10 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True if self._channel_usb: self._state = state.usb_power else: @@ -308,5 +464,5 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 7de9f1459b1..22eb50be86b 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -57,12 +57,24 @@ class Switch(zha.Entity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - await self._endpoint.on_off.on() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch on: %s", ex) + return + self._state = 1 async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch off: %s", ex) + return + self._state = 0 async def async_update(self): diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9e5d4cd9665..e43640e4df2 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==9.0.0'] +REQUIREMENTS = ['python-telegram-bot==10.0.1'] _LOGGER = logging.getLogger(__name__) @@ -63,6 +63,7 @@ DOMAIN = 'telegram_bot' SERVICE_SEND_MESSAGE = 'send_message' SERVICE_SEND_PHOTO = 'send_photo' +SERVICE_SEND_STICKER = 'send_sticker' SERVICE_SEND_VIDEO = 'send_video' SERVICE_SEND_DOCUMENT = 'send_document' SERVICE_SEND_LOCATION = 'send_location' @@ -154,6 +155,7 @@ SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema({ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, @@ -167,10 +169,10 @@ SERVICE_MAP = { def load_data(hass, url=None, filepath=None, username=None, password=None, authentication=None, num_retries=5): - """Load photo/document into ByteIO/File container from a source.""" + """Load data into ByteIO/File container from a source.""" try: if url is not None: - # Load photo from URL + # Load data from URL params = {"timeout": 15} if username is not None and password is not None: if authentication == HTTP_DIGEST_AUTHENTICATION: @@ -181,7 +183,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, while retry_num < num_retries: req = requests.get(url, **params) if not req.ok: - _LOGGER.warning("Status code %s (retry #%s) loading %s.", + _LOGGER.warning("Status code %s (retry #%s) loading %s", req.status_code, retry_num + 1, url) else: data = io.BytesIO(req.content) @@ -189,10 +191,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, data.seek(0) data.name = url return data - _LOGGER.warning("Empty data (retry #%s) in %s).", + _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) retry_num += 1 - _LOGGER.warning("Can't load photo in %s after %s retries.", + _LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): @@ -200,10 +202,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, _LOGGER.warning("'%s' are not secure to load data from!", filepath) else: - _LOGGER.warning("Can't load photo. No photo found in params!") + _LOGGER.warning("Can't load data. No data found in params!") except (OSError, TypeError) as error: - _LOGGER.error("Can't load photo into ByteIO: %s", error) + _LOGGER.error("Can't load data into ByteIO: %s", error) return None @@ -274,9 +276,8 @@ def async_setup(hass, config): if msgtype == SERVICE_SEND_MESSAGE: yield from hass.async_add_job( partial(notify_service.send_message, **kwargs)) - elif (msgtype == SERVICE_SEND_PHOTO or - msgtype == SERVICE_SEND_VIDEO or - msgtype == SERVICE_SEND_DOCUMENT): + elif msgtype in [SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, + SERVICE_SEND_VIDEO, SERVICE_SEND_DOCUMENT]: yield from hass.async_add_job( partial(notify_service.send_file, msgtype, **kwargs)) elif msgtype == SERVICE_SEND_LOCATION: @@ -524,11 +525,12 @@ class TelegramNotificationService: text=message, show_alert=show_alert, **params) def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): - """Send a photo, video, or document.""" + """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) caption = kwargs.get(ATTR_CAPTION) func_send = { SERVICE_SEND_PHOTO: self.bot.sendPhoto, + SERVICE_SEND_STICKER: self.bot.sendSticker, SERVICE_SEND_VIDEO: self.bot.sendVideo, SERVICE_SEND_DOCUMENT: self.bot.sendDocument }.get(file_type) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 4c144fe42db..d8039c0b384 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -59,6 +59,34 @@ send_photo: description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' +send_sticker: + description: Send a sticker. + fields: + url: + description: Remote path to an webp sticker. + example: 'http://example.org/path/to/the/sticker.webp' + file: + description: Local path to an webp sticker. + example: '/path/to/the/sticker.webp' + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + send_video: description: Send a video. fields: diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index 40e4ceffed8..9de7f6c4444 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -116,6 +116,11 @@ class UpCloudServerEntity(Entity): self.uuid = uuid self.data = None + @property + def unique_id(self) -> str: + """Return unique ID for the entity.""" + return self.uuid + @property def name(self): """Return the name of the component.""" diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 1d4ab5eb7ca..b2451ed495c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 139f8abfce6..52aa8c46046 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -19,7 +19,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 47ef2c3eace..1e23ad19897 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -240,7 +240,11 @@ class ActiveConnection: if message is None: break self.debug("Sending", message) - await self.wsock.send_json(message, dumps=JSON_DUMP) + try: + await self.wsock.send_json(message, dumps=JSON_DUMP) + except TypeError as err: + _LOGGER.error('Unable to serialize to JSON: %s\n%s', + err, message) @callback def send_message_outside(self, message): diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf.py index 17c2643ecc3..675c3a65e5f 100644 --- a/homeassistant/components/zeroconf.py +++ b/homeassistant/components/zeroconf.py @@ -12,12 +12,13 @@ import voluptuous as vol from homeassistant import util from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) +REQUIREMENTS = ['zeroconf==0.20.0'] + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['api'] DOMAIN = 'zeroconf' -REQUIREMENTS = ['zeroconf==0.19.1'] ZEROCONF_TYPE = '_home-assistant._tcp.local.' diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 88ca29101ad..39419034545 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,7 +4,6 @@ Support for ZigBee Home Automation devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import asyncio import collections import enum import logging @@ -80,8 +79,7 @@ APPLICATION_CONTROLLER = None _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up ZHA. Will automatically load components to support devices found on the network. @@ -100,35 +98,33 @@ def async_setup(hass, config): from zigpy_xbee.zigbee.application import ControllerApplication radio = zigpy_xbee.api.XBee() - yield from radio.connect(usb_path, baudrate) + await radio.connect(usb_path, baudrate) database = config[DOMAIN].get(CONF_DATABASE) APPLICATION_CONTROLLER = ControllerApplication(radio, database) listener = ApplicationListener(hass, config) APPLICATION_CONTROLLER.add_listener(listener) - yield from APPLICATION_CONTROLLER.startup(auto_form=True) + await APPLICATION_CONTROLLER.startup(auto_form=True) for device in APPLICATION_CONTROLLER.devices.values(): hass.async_add_job(listener.async_device_initialized(device, False)) - @asyncio.coroutine - def permit(service): + async def permit(service): """Allow devices to join this network.""" duration = service.data.get(ATTR_DURATION) _LOGGER.info("Permitting joins for %ss", duration) - yield from APPLICATION_CONTROLLER.permit(duration) + await APPLICATION_CONTROLLER.permit(duration) hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) - @asyncio.coroutine - def remove(service): + async def remove(service): """Remove a node from the network.""" from bellows.types import EmberEUI64, uint8_t ieee = service.data.get(ATTR_IEEE) ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')]) _LOGGER.info("Removing node %s", ieee) - yield from APPLICATION_CONTROLLER.remove(ieee) + await APPLICATION_CONTROLLER.remove(ieee) hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[SERVICE_REMOVE]) @@ -168,8 +164,7 @@ class ApplicationListener: for device_entity in self._device_registry[device.ieee]: self._hass.async_add_job(device_entity.async_remove()) - @asyncio.coroutine - def async_device_initialized(self, device, join): + async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" import zigpy.profiles import homeassistant.components.zha.const as zha_const @@ -179,7 +174,7 @@ class ApplicationListener: if endpoint_id == 0: # ZDO continue - discovered_info = yield from _discover_endpoint_info(endpoint) + discovered_info = await _discover_endpoint_info(endpoint) component = None profile_clusters = ([], []) @@ -218,7 +213,7 @@ class ApplicationListener: discovery_info.update(discovered_info) self._hass.data[DISCOVERY_KEY][device_key] = discovery_info - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, @@ -247,7 +242,7 @@ class ApplicationListener: discovery_info.update(discovered_info) self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, @@ -323,8 +318,7 @@ class Entity(entity.Entity): pass -@asyncio.coroutine -def _discover_endpoint_info(endpoint): +async def _discover_endpoint_info(endpoint): """Find some basic information about an endpoint.""" extra_info = { 'manufacturer': None, @@ -333,20 +327,19 @@ def _discover_endpoint_info(endpoint): if 0 not in endpoint.in_clusters: return extra_info - @asyncio.coroutine - def read(attributes): + async def read(attributes): """Read attributes and update extra_info convenience function.""" - result, _ = yield from endpoint.in_clusters[0].read_attributes( + result, _ = await endpoint.in_clusters[0].read_attributes( attributes, allow_cache=True, ) extra_info.update(result) - yield from read(['manufacturer', 'model']) + await read(['manufacturer', 'model']) if extra_info['manufacturer'] is None or extra_info['model'] is None: # Some devices fail at returning multiple results. Attempt separately. - yield from read(['manufacturer']) - yield from read(['model']) + await read(['manufacturer']) + await read(['model']) for key, value in extra_info.items(): if isinstance(value, bytes): @@ -376,8 +369,7 @@ def get_discovery_info(hass, discovery_info): return all_discovery_info.get(discovery_key, None) -@asyncio.coroutine -def safe_read(cluster, attributes): +async def safe_read(cluster, attributes): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -385,7 +377,7 @@ def safe_read(cluster, attributes): probably only be used during initialization. """ try: - result, _ = yield from cluster.read_attributes( + result, _ = await cluster.read_attributes( attributes, allow_cache=False, ) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index deaa1257396..4fe3581d5b2 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -36,6 +36,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.hvac.Fan: 'fan', }) # A map of hass components to all Zigbee clusters it could use diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index 9ba503e6666..b1a94f3809c 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.loader import bind_hass from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity import Entity, async_generate_entity_id -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 21db39d4e76..a85160e8bde 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import track_time_change -from homeassistant.util import convert, slugify +from homeassistant.util import convert import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -52,7 +52,6 @@ CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' CONF_NETWORK_KEY = 'network_key' -CONF_NEW_ENTITY_IDS = 'new_entity_ids' ATTR_POWER = 'power_consumption' @@ -161,7 +160,6 @@ CONFIG_SCHEMA = vol.Schema({ cv.positive_int, vol.Optional(CONF_USB_STICK_PATH, default=DEFAULT_CONF_USB_STICK_PATH): cv.string, - vol.Optional(CONF_NEW_ENTITY_IDS, default=True): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -178,27 +176,6 @@ def _value_name(value): return '{} {}'.format(node_name(value.node), value.label).strip() -def _node_object_id(node): - """Return the object_id of the node.""" - node_object_id = '{}_{}'.format(slugify(node_name(node)), node.node_id) - return node_object_id - - -def object_id(value): - """Return the object_id of the device value. - - The object_id contains node_id and value instance id - to not collide with other entity_ids. - """ - _object_id = "{}_{}_{}".format(slugify(_value_name(value)), - value.node.node_id, value.index) - - # Add the instance id if there is more than one instance for the value - if value.instance > 1: - return '{}_{}'.format(_object_id, value.instance) - return _object_id - - def nice_print_node(node): """Print a nice formatted node to the output (debug method).""" node_dict = _obj_to_dict(node) @@ -226,8 +203,8 @@ def get_config_value(node, value_index, tries=5): return None -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Z-Wave platform (generic part).""" if discovery_info is None or DATA_NETWORK not in hass.data: return False @@ -260,13 +237,6 @@ def setup(hass, config): config[DOMAIN][CONF_DEVICE_CONFIG], config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN], config[DOMAIN][CONF_DEVICE_CONFIG_GLOB]) - new_entity_ids = config[DOMAIN][CONF_NEW_ENTITY_IDS] - if not new_entity_ids: - _LOGGER.warning( - "ZWave entity_ids will soon be changing. To opt in to new " - "entity_ids now, set `new_entity_ids: true` under zwave in your " - "configuration.yaml. See the following blog post for details: " - "https://home-assistant.io/blog/2017/06/15/zwave-entity-ids/") # Setup options options = ZWaveOption( @@ -328,12 +298,9 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" - entity = ZWaveNodeEntity(node, network, new_entity_ids) + entity = ZWaveNodeEntity(node, network) name = node_name(node) - if new_entity_ids: - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - else: - generated_id = entity.entity_id + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) node_config = device_config.get(generated_id) if node_config.get(CONF_IGNORED): _LOGGER.info( @@ -475,9 +442,16 @@ def setup(hass, config): if value.index != param: continue if value.type in [const.TYPE_LIST, const.TYPE_BOOL]: - value.data = selection - _LOGGER.info("Setting config list parameter %s on Node %s " - "with selection %s", param, node_id, + value.data = str(selection) + _LOGGER.info("Setting config parameter %s on Node %s " + "with list/bool selection %s", param, node_id, + str(selection)) + return + if value.type == const.TYPE_BUTTON: + network.manager.pressButton(value.value_id) + network.manager.releaseButton(value.value_id) + _LOGGER.info("Setting config parameter %s on Node %s " + "with button selection %s", param, node_id, selection) return value.data = int(selection) @@ -537,8 +511,7 @@ def setup(hass, config): "target node:%s, instance=%s", node_id, group, target_node_id, instance) - @asyncio.coroutine - def async_refresh_entity(service): + async def async_refresh_entity(service): """Refresh values that specific entity depends on.""" entity_id = service.data.get(ATTR_ENTITY_ID) async_dispatcher_send( @@ -592,8 +565,7 @@ def setup(hass, config): network.start() hass.bus.fire(const.EVENT_NETWORK_START) - @asyncio.coroutine - def _check_awaked(): + async def _check_awaked(): """Wait for Z-wave awaked state (or timeout) and finalize start.""" _LOGGER.debug( "network state: %d %s", network.state, @@ -618,7 +590,7 @@ def setup(hass, config): network.state_str) break else: - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) hass.async_add_job(_finalize_start) @@ -794,11 +766,7 @@ class ZWaveDeviceEntityValues(): component = workaround_component value_name = _value_name(self.primary) - if self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: - generated_id = generate_entity_id( - component + '.{}', value_name, []) - else: - generated_id = "{}.{}".format(component, object_id(self.primary)) + generated_id = generate_entity_id(component + '.{}', value_name, []) node_config = self._device_config.get(generated_id) # Configure node @@ -831,21 +799,14 @@ class ZWaveDeviceEntityValues(): self._workaround_ignore = True return - device.old_entity_id = "{}.{}".format( - component, object_id(self.primary)) - device.new_entity_id = "{}.{}".format(component, slugify(device.name)) - if not self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: - device.entity_id = device.old_entity_id - self._entity = device dict_id = id(self) - @asyncio.coroutine - def discover_device(component, device, dict_id): + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) self._hass.add_job(discover_device, component, device, dict_id) @@ -887,8 +848,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.update_properties() self.maybe_schedule_update() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add device to dict.""" async_dispatcher_connect( self.hass, @@ -932,8 +892,6 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): const.ATTR_VALUE_INDEX: self.values.primary.index, const.ATTR_VALUE_INSTANCE: self.values.primary.instance, const.ATTR_VALUE_ID: str(self.values.primary.value_id), - 'old_entity_id': self.old_entity_id, - 'new_entity_id': self.new_entity_id, } if self.power_consumption is not None: diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index e2524aefadf..8e1a22047c1 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -7,7 +7,6 @@ ATTR_ASSOCIATION = "association" ATTR_INSTANCE = "instance" ATTR_GROUP = "group" ATTR_VALUE_ID = "value_id" -ATTR_OBJECT_ID = "object_id" ATTR_MESSAGES = "messages" ATTR_NAME = "name" ATTR_RETURN_ROUTES = "return_routes" @@ -328,6 +327,7 @@ TYPE_DECIMAL = "Decimal" TYPE_INT = "Int" TYPE_LIST = "List" TYPE_STRING = "String" +TYPE_BUTTON = "Button" DISC_COMMAND_CLASS = "command_class" DISC_COMPONENT = "component" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index de8ca0c1ab9..5a4b1b02504 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -4,11 +4,10 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, - ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, DOMAIN, + ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) from .util import node_name @@ -41,8 +40,6 @@ class ZWaveBaseEntity(Entity): def __init__(self): """Initialize the base Z-Wave class.""" self._update_scheduled = False - self.old_entity_id = None - self.new_entity_id = None def maybe_schedule_update(self): """Maybe schedule state update. @@ -72,7 +69,7 @@ class ZWaveBaseEntity(Entity): class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" - def __init__(self, node, network, new_entity_ids): + def __init__(self, node, network): """Initialize node.""" # pylint: disable=import-error super().__init__() @@ -84,11 +81,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name - self.old_entity_id = "{}.{}_{}".format( - DOMAIN, slugify(self._name), self.node_id) - self.new_entity_id = "{}.{}".format(DOMAIN, slugify(self._name)) - if not new_entity_ids: - self.entity_id = self.old_entity_id self._attributes = {} self.wakeup_interval = None self.location = None @@ -229,8 +221,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): ATTR_NODE_NAME: self._name, ATTR_MANUFACTURER_NAME: self._manufacturer_name, ATTR_PRODUCT_NAME: self._product_name, - 'old_entity_id': self.old_entity_id, - 'new_entity_id': self.new_entity_id, } attrs.update(self._attributes) if self.battery_level is not None: diff --git a/homeassistant/config.py b/homeassistant/config.py index e94fc297f48..28936ae12e9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,5 +1,4 @@ """Module to help with parsing and generating configuration files.""" -import asyncio from collections import OrderedDict # pylint: disable=no-name-in-module from distutils.version import LooseVersion # pylint: disable=import-error @@ -7,7 +6,6 @@ import logging import os import re import shutil -import sys # pylint: disable=unused-import from typing import Any, List, Tuple # NOQA @@ -15,6 +13,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, @@ -131,13 +130,19 @@ PACKAGES_CONFIG_SCHEMA = vol.Schema({ {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names }) +CUSTOMIZE_DICT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.entity_id: dict}), + vol.Schema({cv.entity_id: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): - vol.Schema({cv.string: dict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: OrderedDict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ @@ -549,6 +554,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if hasattr(component, 'PLATFORM_SCHEMA'): + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -557,6 +564,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): merge_type, _ = _identify_config_schema(component) if merge_type == 'list': + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -665,22 +674,14 @@ async def async_check_ha_config_file(hass): This method is a coroutine. """ - proc = await asyncio.create_subprocess_exec( - sys.executable, '-m', 'homeassistant', '--script', - 'check_config', '--config', hass.config.config_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, loop=hass.loop) + from homeassistant.scripts.check_config import check_ha_config_file - # Wait for the subprocess exit - log, _ = await proc.communicate() - exit_code = await proc.wait() + res = await hass.async_add_job( + check_ha_config_file, hass.config.config_dir) - # Convert to ASCII - log = RE_ASCII.sub('', log.decode()) - - if exit_code != 0 or RE_YAML_ERROR.search(log): - return log - return None + if not res.errors: + return None + return '\n'.join([err.message for err in res.errors]) @callback diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 230e48f0cec..eb05e800683 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -442,7 +442,7 @@ class FlowManager: 'Handler returned incorrect type: {}'.format(result['type'])) if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result.pop('step_id'), result['data_schema']) + flow.cur_step = (result['step_id'], result['data_schema']) return result # Abort and Success results both finish the flow @@ -468,6 +468,7 @@ class ConfigFlowHandler: # Set by flow manager flow_id = None hass = None + domain = None source = SOURCE_USER cur_step = None @@ -475,15 +476,13 @@ class ConfigFlowHandler: # VERSION @callback - def async_show_form(self, *, title, step_id, description=None, - data_schema=None, errors=None): + def async_show_form(self, *, step_id, data_schema=None, errors=None): """Return the definition of a form to gather user input.""" return { 'type': RESULT_TYPE_FORM, 'flow_id': self.flow_id, - 'title': title, + 'domain': self.domain, 'step_id': step_id, - 'description': description, 'data_schema': data_schema, 'errors': errors, } @@ -494,6 +493,7 @@ class ConfigFlowHandler: return { 'type': RESULT_TYPE_CREATE_ENTRY, 'flow_id': self.flow_id, + 'domain': self.domain, 'title': title, 'data': data, } @@ -504,5 +504,6 @@ class ConfigFlowHandler: return { 'type': RESULT_TYPE_ABORT, 'flow_id': self.flow_id, + 'domain': self.domain, 'reason': reason } diff --git a/homeassistant/const.py b/homeassistant/const.py index 7fe26e6c334..3dce8882015 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 = 65 -PATCH_VERSION = '6' +MINOR_VERSION = 66 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) @@ -27,6 +27,7 @@ CONF_ADDRESS = 'address' CONF_AFTER = 'after' CONF_ALIAS = 'alias' CONF_API_KEY = 'api_key' +CONF_API_VERSION = 'api_version' CONF_AT = 'at' CONF_AUTHENTICATION = 'authentication' CONF_BASE = 'base' diff --git a/homeassistant/core.py b/homeassistant/core.py index 543aba2a0e7..feb8d331ae8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError) -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe, fire_coroutine_threadsafe) import homeassistant.util as util @@ -79,7 +79,7 @@ def callback(func: Callable[..., None]) -> Callable[..., None]: def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in func.__dict__ + return '_hass_callback' in getattr(func, '__dict__', {}) @callback @@ -117,11 +117,7 @@ class HomeAssistant(object): else: self.loop = loop or asyncio.get_event_loop() - executor_opts = {'max_workers': 10} - if sys.version_info[:2] >= (3, 5): - # It will default set to the number of processors on the machine, - # multiplied by 5. That is better for overlap I/O workers. - executor_opts['max_workers'] = None + executor_opts = {'max_workers': None} if sys.version_info[:2] >= (3, 6): executor_opts['thread_name_prefix'] = 'SyncWorker' @@ -1064,15 +1060,19 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path) + thepath = pathlib.Path(path) try: - parent = parent.resolve() # pylint: disable=no-member + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() except (FileNotFoundError, RuntimeError, PermissionError): return False for whitelisted_path in self.whitelist_external_dirs: try: - parent.relative_to(whitelisted_path) + thepath.relative_to(whitelisted_path) return True except ValueError: pass diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 72f2214b5e7..bb34942ad79 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -149,34 +149,27 @@ def _async_get_connector(hass, verify_ssl=True): This method must be run in the event loop. """ - is_new = False + key = DATA_CONNECTOR if verify_ssl else DATA_CONNECTOR_NOTVERIFY + + if key in hass.data: + return hass.data[key] if verify_ssl: - if DATA_CONNECTOR not in hass.data: - ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_context.load_verify_locations(cafile=certifi.where(), - capath=None) - connector = aiohttp.TCPConnector(loop=hass.loop, - ssl_context=ssl_context) - hass.data[DATA_CONNECTOR] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR] + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_verify_locations(cafile=certifi.where(), + capath=None) else: - if DATA_CONNECTOR_NOTVERIFY not in hass.data: - connector = aiohttp.TCPConnector(loop=hass.loop, verify_ssl=False) - hass.data[DATA_CONNECTOR_NOTVERIFY] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR_NOTVERIFY] + ssl_context = False - if is_new: - @callback - def _async_close_connector(event): - """Close connector pool.""" - connector.close() + connector = aiohttp.TCPConnector(loop=hass.loop, ssl=ssl_context) + hass.data[key] = connector - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) + @callback + def _async_close_connector(event): + """Close connector pool.""" + connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) return connector diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index bad6bfe83c3..f8f841cc449 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe FROM_CONFIG_FORMAT = '{}_from_config' ASYNC_FROM_CONFIG_FORMAT = 'async_{}_from_config' diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 6a527021c77..cb587c432c1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,15 +5,13 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -import asyncio - from homeassistant import setup, core from homeassistant.loader import bind_hass from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import DEPENDENCY_BLACKLIST -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe EVENT_LOAD_PLATFORM = 'load_platform.{}' ATTR_PLATFORM = 'platform' @@ -58,17 +56,16 @@ def discover(hass, service, discovered=None, component=None, hass_config=None): async_discover(hass, service, discovered, component, hass_config)) -@asyncio.coroutine @bind_hass -def async_discover(hass, service, discovered=None, component=None, - hass_config=None): +async def async_discover(hass, service, discovered=None, component=None, + hass_config=None): """Fire discovery event. Can ensure a component is loaded.""" if component in DEPENDENCY_BLACKLIST: raise HomeAssistantError( 'Cannot discover the {} component.'.format(component)) if component is not None and component not in hass.config.components: - yield from setup.async_setup_component( + await setup.async_setup_component( hass, component, hass_config) data = { @@ -134,10 +131,9 @@ def load_platform(hass, component, platform, discovered=None, hass_config)) -@asyncio.coroutine @bind_hass -def async_load_platform(hass, component, platform, discovered=None, - hass_config=None): +async def async_load_platform(hass, component, platform, discovered=None, + hass_config=None): """Load a component and platform dynamically. Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be @@ -148,7 +144,7 @@ def async_load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. - Warning: Do not yield from this inside a setup method to avoid a dead lock. + Warning: Do not await this inside a setup method to avoid a dead lock. Use `hass.async_add_job(async_load_platform(..))` instead. This method is a coroutine. @@ -160,7 +156,7 @@ def async_load_platform(hass, component, platform, discovered=None, setup_success = True if component not in hass.config.components: - setup_success = yield from setup.async_setup_component( + setup_success = await setup.async_setup_component( hass, component, hass_config) # No need to fire event if we could not setup component diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 8c41505bd29..136f4caa35a 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -3,7 +3,7 @@ import logging from homeassistant.core import callback from homeassistant.loader import bind_hass -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f23a49c1851..efaefc26184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,7 +4,7 @@ import logging import functools as ft from timeit import default_timer as timer -from typing import Optional, List +from typing import Optional, List, Iterable from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 @@ -42,7 +42,7 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], @callback def async_generate_entity_id(entity_id_format: str, name: Optional[str], - current_ids: Optional[List[str]] = None, + current_ids: Optional[Iterable[str]] = None, hass: Optional[HomeAssistant] = None) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" if current_ids is None: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d28212a34d1..501ab5057a3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) import homeassistant.util.dt as dt_util @@ -311,7 +311,7 @@ class EntityPlatform(object): self.scan_interval) return - with (await self._process_updates): + async with self._process_updates: tasks = [] for entity in self.entities.values(): if not entity.should_poll: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index eab2d583f45..d69a556b0cc 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -8,7 +8,7 @@ from ..core import HomeAssistant, callback from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) from ..util import dt as dt_util -from ..util.async import run_callback_threadsafe +from ..util.async_ import run_callback_threadsafe # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index aac00b07d7a..eb88a3db369 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -75,7 +75,7 @@ async def async_get_last_state(hass, entity_id: str): if _LOCK not in hass.data: hass.data[_LOCK] = asyncio.Lock(loop=hass.loop) - with (await hass.data[_LOCK]): + async with hass.data[_LOCK]: if DATA_RESTORE_CACHE not in hass.data: await hass.async_add_job( _load_restore_cache, hass) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6530cb62485..f2ae36e7fd0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template) from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as date_util -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe) _LOGGER = logging.getLogger(__name__) @@ -97,11 +97,16 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, template.Template): - delay = vol.All( - cv.time_period, - cv.positive_timedelta)( - delay.async_render(variables)) + try: + if isinstance(delay, template.Template): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + delay.async_render(variables)) + except (TemplateError, vol.Invalid) as ex: + _LOGGER.error("Error rendering '%s' delay template: %s", + self.name, ex) + break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7118cab211a..3595b258f12 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -13,7 +13,7 @@ from homeassistant.helpers import template from homeassistant.loader import get_component, bind_hass from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe CONF_SERVICE = 'service' CONF_SERVICE_TEMPLATE = 'service_template' diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 6be0dbae914..f97d7051459 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -40,7 +40,7 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION) from homeassistant.core import State -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6fab1c6c844..28ab4e9bfa0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,14 +13,14 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, STATE_UNKNOWN) -from homeassistant.core import State +from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import bind_hass, get_component from homeassistant.util import convert from homeassistant.util import dt as dt_util from homeassistant.util import location as loc_util -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -73,7 +73,8 @@ def extract_entities(template, variables=None): extraction_final.append(result[0]) if variables and result[1] in variables and \ - isinstance(variables[result[1]], str): + isinstance(variables[result[1]], str) and \ + valid_entity_id(variables[result[1]]): extraction_final.append(variables[result[1]]) if extraction_final: diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 9d1773de4d2..26cb34ede8c 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -4,6 +4,7 @@ import logging from typing import Optional # NOQA from os import path +from homeassistant import config_entries from homeassistant.loader import get_component, bind_hass from homeassistant.util.json import load_json @@ -89,7 +90,7 @@ async def async_get_component_resources(hass, language): translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] # Get the set of components - components = hass.config.components + components = hass.config.components | set(config_entries.FLOWS) # Calculate the missing components missing_components = components - set(translation_cache) diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 5aa051f2bb5..d5c629c9d34 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -61,7 +61,7 @@ def disable_c_asyncio(): def find_module(self, fullname, path=None): """Find a module.""" if fullname == self.PATH_TRIGGER: - # We lint in Py34, exception is introduced in Py36 + # We lint in Py35, exception is introduced in Py36 # pylint: disable=undefined-variable raise ModuleNotFoundError() # noqa return None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 16b8815e5cf..e43e1f3dafe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,9 +5,9 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.6 -async_timeout==2.0.0 -astral==1.5 +aiohttp==3.0.9 +async_timeout==2.0.1 +astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 566f37a621a..5a33bd58641 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -123,17 +123,7 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(o, 'as_dict'): return o.as_dict() - try: - return json.JSONEncoder.default(self, o) - except TypeError: - # If the JSON serializer couldn't serialize it - # it might be a generator, convert it to a list - try: - return [self.default(child_obj) - for child_obj in o] - except TypeError: - # Ok, we're lost, cause the original error - return json.JSONEncoder.default(self, o) + return json.JSONEncoder.default(self, o) def validate_api(api): diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 641693501ff..ac3ac62e82d 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -95,9 +95,12 @@ def run(script_args: List) -> int: if args.files: print(color(C_HEAD, 'yaml files'), '(used /', color('red', 'not used') + ')') - # Python 3.5 gets a recursive, but not in 3.4 - for yfn in sorted(glob(os.path.join(config_dir, '*.yaml')) + - glob(os.path.join(config_dir, '*/*.yaml'))): + deps = os.path.join(config_dir, 'deps') + yaml_files = [f for f in glob(os.path.join(config_dir, '**/*.yaml'), + recursive=True) + if not f.startswith(deps)] + + for yfn in sorted(yaml_files): the_color = '' if yfn in res['yaml_files'] else 'red' print(color(the_color, '-', yfn)) @@ -140,6 +143,9 @@ def run(script_args: List) -> int: print(color(C_HEAD, 'Used Secrets:')) for skey, sval in res['secrets'].items(): + if sval is None: + print(' -', skey + ':', color('red', "not found")) + continue print(' -', skey + ':', sval, color('cyan', '[from:', flatsecret .get(skey, 'keyring') + ']')) @@ -308,7 +314,8 @@ def check_ha_config_file(config_dir): return result.add_error("File configuration.yaml not found.") config = load_yaml_config_file(config_path) except HomeAssistantError as err: - return result.add_error(err) + return result.add_error( + "Error loading {}: {}".format(config_path, err)) finally: yaml.clear_secret_cache() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5be1547242e..169a160af65 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,7 @@ from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a869251dc3c..a8a84c6c880 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -13,7 +13,7 @@ from functools import wraps from types import MappingProxyType from unicodedata import normalize -from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union +from typing import Any, Optional, TypeVar, Callable, KeysView, Union, Iterable from .dt import as_local, utcnow @@ -72,7 +72,7 @@ def convert(value: T, to_type: Callable[[T], U], def ensure_unique_string(preferred_string: str, current_strings: - Union[Sequence[str], KeysView[str]]) -> str: + Union[Iterable[str], KeysView[str]]) -> str: """Return a string that is not present in current_strings. If preferred string exists will append _2, _3, .. @@ -261,6 +261,16 @@ class Throttle(object): def __call__(self, method): """Caller for the throttle.""" + # Make sure we return a coroutine if the method is async. + if asyncio.iscoroutinefunction(method): + async def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + else: + def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + if self.limit_no_throttle is not None: method = Throttle(self.limit_no_throttle)(method) @@ -277,16 +287,6 @@ class Throttle(object): is_func = (not hasattr(method, '__self__') and '.' not in method.__qualname__.split('..')[-1]) - # Make sure we return a coroutine if the method is async. - if asyncio.iscoroutinefunction(method): - async def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - else: - def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - @wraps(method) def wrapper(*args, **kwargs): """Wrap that allows wrapped to be called only once per min_time. diff --git a/homeassistant/util/async.py b/homeassistant/util/async_.py similarity index 95% rename from homeassistant/util/async.py rename to homeassistant/util/async_.py index ea8e5e3c874..5676a1d0844 100644 --- a/homeassistant/util/async.py +++ b/homeassistant/util/async_.py @@ -5,14 +5,7 @@ import logging from asyncio import coroutines from asyncio.futures import Future -try: - # pylint: disable=ungrouped-imports - from asyncio import ensure_future -except ImportError: - # Python 3.4.3 and earlier has this as async - # pylint: disable=unused-import - from asyncio import async - ensure_future = async +from asyncio import ensure_future _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 70863a0ab90..c2e4ac737e8 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,11 +173,18 @@ def color_name_to_rgb(color_name): return hex_value +# pylint: disable=invalid-name, invalid-sequence-index +def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert from RGB color to XY color.""" + return color_RGB_to_xy_brightness(iR, iG, iB)[:2] + + # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name, invalid-sequence-index -def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: +def color_RGB_to_xy_brightness( + iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" if iR + iG + iB == 0: return 0.0, 0.0, 0 @@ -210,6 +217,11 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: return round(x, 3), round(y, 3), brightness +def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: + """Convert from XY to a normalized RGB.""" + return color_xy_brightness_to_RGB(vX, vY, 255) + + # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # pylint: disable=invalid-sequence-index @@ -307,6 +319,12 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) +# pylint: disable=invalid-sequence-index +def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert an rgb color to its hs representation.""" + return color_RGB_to_hsv(iR, iG, iB)[:2] + + # pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -320,12 +338,24 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: # pylint: disable=invalid-sequence-index -def color_xy_to_hs(vX: float, vY: float) -> Tuple[int, int]: +def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + return color_hsv_to_RGB(iH, iS, 100) + + +# pylint: disable=invalid-sequence-index +def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" - h, s, _ = color_RGB_to_hsv(*color_xy_brightness_to_RGB(vX, vY, 255)) + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) +# pylint: disable=invalid-sequence-index +def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: + """Convert an hs color to its xy representation.""" + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) + + # pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: @@ -374,6 +404,11 @@ def rgb_hex_to_rgb_list(hex_string): len(hex_string) // 3)] +def color_temperature_to_hs(color_temperature_kelvin): + """Return an hs color from a color temperature in Kelvin.""" + return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) + + def color_temperature_to_rgb(color_temperature_kelvin): """ Return an RGB color from a color temperature in Kelvin. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 0cd0b14d3ab..dae8ed17dc9 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -10,7 +10,7 @@ from typing import Any, Optional, Tuple, Dict import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -FREEGEO_API = 'https://freegeoip.io/json/' +FREEGEO_API = 'https://freegeoip.net/json/' IP_API = 'http://ip-api.com/json' # Constants from https://github.com/maurycyp/vincenty diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 8a15c4f6320..f7306cae98b 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -3,7 +3,7 @@ import asyncio import logging import threading -from .async import run_coroutine_threadsafe +from .async_ import run_coroutine_threadsafe class HideSensitiveDataFilter(logging.Filter): diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 8ac8d096b99..66d673987a3 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -288,8 +288,7 @@ def _secret_yaml(loader: SafeLineLoader, # Catch if package installed and no config credstash = None - _LOGGER.error("Secret %s not defined", node.value) - raise HomeAssistantError(node.value) + raise HomeAssistantError("Secret {} not defined".format(node.value)) yaml.SafeLoader.add_constructor('!include', _include_yaml) diff --git a/requirements_all.txt b/requirements_all.txt index cc4e7dbd708..7ac9bd5fd7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,9 +6,9 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.6 -async_timeout==2.0.0 -astral==1.5 +aiohttp==3.0.9 +async_timeout==2.0.1 +astral==1.6 certifi>=2017.4.17 attrs==17.4.0 @@ -19,7 +19,7 @@ attrs==17.4.0 # Adafruit_BBIO==1.0.0 # homeassistant.components.doorbird -DoorBirdPy==0.1.2 +DoorBirdPy==0.1.3 # homeassistant.components.homekit HAP-python==1.1.7 @@ -52,7 +52,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.8 +TwitterAPI==2.5.0 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 @@ -71,10 +71,10 @@ aiodns==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.3.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 @@ -189,6 +189,16 @@ colorlog==3.1.2 # homeassistant.components.binary_sensor.concord232 concord232==0.15 +# homeassistant.components.climate.eq3btsmart +# homeassistant.components.fan.xiaomi_miio +# homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.eddystone_temperature +# homeassistant.components.sensor.xiaomi_miio +# homeassistant.components.switch.xiaomi_miio +# homeassistant.components.vacuum.xiaomi_miio +construct==2.9.41 + # homeassistant.scripts.credstash # credstash==1.14.0 @@ -287,6 +297,9 @@ fixerio==0.1.1 # homeassistant.components.light.flux_led flux_led==0.21 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.notify.free_mobile freesms==0.1.2 @@ -332,7 +345,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.1 +ha-philipsjs==0.0.2 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 @@ -350,10 +363,13 @@ hikvision==0.4 hipnotify==1.0.8 # homeassistant.components.binary_sensor.workday -holidays==0.9.3 +holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180330.0 + +# homeassistant.components.homematicip_cloud +homematicip==0.8 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -411,7 +427,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.2 +insteonplm==0.8.3 # homeassistant.components.verisure jsonpath==0.75 @@ -512,7 +528,7 @@ myusps==1.3.2 nad_receiver==0.0.9 # homeassistant.components.discovery -netdisco==1.2.4 +netdisco==1.3.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -525,7 +541,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.google oauth2client==4.0.0 @@ -565,9 +581,6 @@ pdunehd==1.3 # homeassistant.components.media_player.pandora pexpect==4.0.1 -# homeassistant.components.hue -phue==1.0 - # homeassistant.components.rpi_pfio pifacecommon==4.1.2 @@ -645,7 +658,7 @@ pyHS100==0.3.0 pyRFXtrx==0.21.1 # homeassistant.components.sensor.tibber -pyTibber==0.3.2 +pyTibber==0.4.0 # homeassistant.components.switch.dlink pyW215==0.6.0 @@ -682,7 +695,7 @@ pybbox==0.0.5-alpha pychannels==1.0.0 # homeassistant.components.media_player.cast -pychromecast==2.0.0 +pychromecast==2.1.0 # homeassistant.components.media_player.cmus pycmus==0.1.0 @@ -701,7 +714,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==30 +pydeconz==32 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -749,7 +762,7 @@ pyhik==0.1.8 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.39 +pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 @@ -801,7 +814,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.media_player.mediaroom -pymediaroom==0.5 +pymediaroom==0.6 # homeassistant.components.media_player.xiaomi_tv pymitv==1.0.0 @@ -888,6 +901,12 @@ pysma==0.2 # homeassistant.components.switch.snmp pysnmp==4.4.4 +# homeassistant.components.notify.stride +pystride==0.1.7 + +# homeassistant.components.sensor.syncthru +pysyncthru==0.3.1 + # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 @@ -905,7 +924,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.15 +python-ecobee-api==0.0.17 # homeassistant.components.climate.eq3btsmart # python-eq3bt==0.1.9 @@ -915,7 +934,7 @@ python-etherscan-api==0.0.3 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.gc100 python-gc100==1.0.3a @@ -936,9 +955,10 @@ python-juicenet==0.0.5 # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.7 +python-miio==0.3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 @@ -948,7 +968,7 @@ python-mpd2==0.5.5 python-mystrom==0.3.8 # homeassistant.components.nest -python-nest==3.1.0 +python-nest==3.7.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 @@ -975,7 +995,7 @@ python-synology==0.1.0 python-tado==0.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==9.0.0 +python-telegram-bot==10.0.1 # homeassistant.components.sensor.twitch python-twitch==1.3.0 @@ -996,8 +1016,7 @@ python_opendata_transport==0.0.3 python_openzwave==0.4.3 # homeassistant.components.egardia -# homeassistant.components.alarm_control_panel.egardia -pythonegardia==1.0.38 +pythonegardia==1.0.39 # homeassistant.components.sensor.whois pythonwhois==2.4.3 @@ -1023,6 +1042,9 @@ pyunifi==2.13 # homeassistant.components.vera pyvera==0.2.42 +# homeassistant.components.switch.vesync +pyvesync==0.1.1 + # homeassistant.components.media_player.vizio pyvizio==0.0.2 @@ -1036,7 +1058,7 @@ pywebpush==1.6.0 pywemo==0.4.25 # homeassistant.components.camera.xeoma -pyxeoma==1.3 +pyxeoma==1.4.0 # homeassistant.components.zabbix pyzabbix==0.7.4 @@ -1093,7 +1115,7 @@ samsungctl[websocket]==0.7.1 satel_integra==0.1.0 # homeassistant.components.sensor.deutsche_bahn -schiene==0.21 +schiene==0.22 # homeassistant.components.scsgate scsgate==0.1.0 @@ -1153,12 +1175,12 @@ somecomfort==0.5.0 speedtest-cli==2.0.0 # homeassistant.components.sensor.spotcrime -spotcrime==1.0.2 +spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 @@ -1298,13 +1320,13 @@ yeelight==0.4.0 yeelightsunflower==0.0.8 # homeassistant.components.media_extractor -youtube_dl==2018.02.11 +youtube_dl==2018.03.10 # homeassistant.components.light.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.19.1 +zeroconf==0.20.0 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 diff --git a/requirements_docs.txt b/requirements_docs.txt index 60946fd00a8..bb0d30462ce 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.0 +Sphinx==1.7.1 sphinx-autodoc-typehints==1.2.5 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test.txt b/requirements_test.txt index d56a7085c74..fc9e113e97c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,17 +1,17 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -flake8==3.5 -pylint==1.8.2 -mypy==0.560 -pydocstyle==1.1.1 +asynctest>=0.11.1 coveralls==1.2.0 -pytest==3.3.1 +flake8-docstrings==1.0.3 +flake8==3.5 +mock-open==1.3.1 +mypy==0.570 +pydocstyle==1.1.1 +pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 +pytest-sugar==0.9.1 pytest-timeout>=1.2.1 -pytest-sugar==0.9.0 +pytest==3.4.2 requests_mock==1.4 -mock-open==1.3.1 -flake8-docstrings==1.0.3 -asynctest>=0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 460e70cbca5..33527a913a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,20 +2,20 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -flake8==3.5 -pylint==1.8.2 -mypy==0.560 -pydocstyle==1.1.1 +asynctest>=0.11.1 coveralls==1.2.0 -pytest==3.3.1 +flake8-docstrings==1.0.3 +flake8==3.5 +mock-open==1.3.1 +mypy==0.570 +pydocstyle==1.1.1 +pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 +pytest-sugar==0.9.1 pytest-timeout>=1.2.1 -pytest-sugar==0.9.0 +pytest==3.4.2 requests_mock==1.4 -mock-open==1.3.1 -flake8-docstrings==1.0.3 -asynctest>=0.11.1 # homeassistant.components.homekit @@ -32,10 +32,10 @@ aioautomatic==0.6.5 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.3.0 # homeassistant.components.notify.apns apns2==0.3.0 @@ -62,6 +62,9 @@ evohomeclient==0.2.5 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.tts.google gTTS-token==1.1.1 @@ -75,10 +78,10 @@ haversine==0.4.5 hbmqtt==0.9.1 # homeassistant.components.binary_sensor.workday -holidays==0.9.3 +holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180330.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -96,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -141,7 +144,7 @@ pynx584==0.4 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.sensor.whois pythonwhois==2.4.3 @@ -173,7 +176,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a9a68d09491..d8fc7b1ed60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -47,6 +47,7 @@ TEST_REQUIREMENTS = ( 'ephem', 'evohomeclient', 'feedparser', + 'foobot_async', 'gTTS-token', 'HAP-python', 'ha-ffmpeg', @@ -277,23 +278,23 @@ def validate_constraints_file(data): return data + CONSTRAINT_BASE == req_file.read() -def main(): +def main(validate): """Main section of the script.""" if not os.path.isfile('requirements_all.txt'): print('Run this from HA root dir') - return + return 1 data = gather_modules() if data is None: - sys.exit(1) + return 1 constraints = gather_constraints() reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) - if sys.argv[-1] == 'validate': + if validate: errors = [] if not validate_requirements_file(reqs_file): errors.append("requirements_all.txt is not up to date") @@ -309,14 +310,16 @@ def main(): print("******* ERROR") print('\n'.join(errors)) print("Please run script/gen_requirements_all.py") - sys.exit(1) + return 1 - sys.exit(0) + return 0 write_requirements_file(reqs_file) write_test_requirements_file(reqs_test_file) write_constraints_file(constraints) + return 0 if __name__ == '__main__': - main() + _VAL = sys.argv[-1] == 'validate' + sys.exit(main(_VAL)) diff --git a/script/lazytox.py b/script/lazytox.py new file mode 100755 index 00000000000..2639d640753 --- /dev/null +++ b/script/lazytox.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Lazy 'tox' to quickly check if branch is up to PR standards. + +This is NOT a tox replacement, only a quick check during development. +""" +import os +import asyncio +import sys +import re +import shlex +from collections import namedtuple + +try: + from colorlog.escape_codes import escape_codes +except ImportError: + escape_codes = None + + +RE_ASCII = re.compile(r"\033\[[^m]*m") +Error = namedtuple('Error', ['file', 'line', 'col', 'msg']) + +PASS = 'green' +FAIL = 'bold_red' + + +def printc(the_color, *args): + """Color print helper.""" + msg = ' '.join(args) + if not escape_codes: + print(msg) + return + try: + print(escape_codes[the_color] + msg + escape_codes['reset']) + except KeyError: + print(msg) + raise ValueError("Invalid color {}".format(the_color)) + + +def validate_requirements_ok(): + """Validate requirements, returns True of ok.""" + # pylint: disable=E0402 + from gen_requirements_all import main as req_main + return req_main(True) == 0 + + +async def read_stream(stream, display): + """Read from stream line by line until EOF, display, and capture lines.""" + output = [] + while True: + line = await stream.readline() + if not line: + break + output.append(line) + display(line.decode()) # assume it doesn't block + return b''.join(output) + + +async def async_exec(*args, display=False): + """Execute, return code & log.""" + argsp = [] + for arg in args: + if os.path.isfile(arg): + argsp.append("\\\n {}".format(shlex.quote(arg))) + else: + argsp.append(shlex.quote(arg)) + printc('cyan', *argsp) + try: + kwargs = {'loop': LOOP, 'stdout': asyncio.subprocess.PIPE, + 'stderr': asyncio.subprocess.STDOUT} + if display: + kwargs['stderr'] = asyncio.subprocess.PIPE + # pylint: disable=E1120 + proc = await asyncio.create_subprocess_exec(*args, **kwargs) + except FileNotFoundError as err: + printc(FAIL, "Could not execute {}. Did you install test requirements?" + .format(args[0])) + raise err + + if not display: + # Readin stdout into log + stdout, _ = await proc.communicate() + else: + # read child's stdout/stderr concurrently (capture and display) + stdout, _ = await asyncio.gather( + read_stream(proc.stdout, sys.stdout.write), + read_stream(proc.stderr, sys.stderr.write)) + exit_code = await proc.wait() + stdout = stdout.decode('utf-8') + return exit_code, stdout + + +async def git(): + """Exec git.""" + if len(sys.argv) > 2 and sys.argv[1] == '--': + return sys.argv[2:] + _, log = await async_exec('git', 'merge-base', 'upstream/dev', 'HEAD') + merge_base = log.splitlines()[0] + _, log = await async_exec('git', 'diff', merge_base, '--name-only') + return log.splitlines() + + +async def pylint(files): + """Exec pylint.""" + _, log = await async_exec('pylint', '-f', 'parseable', '--persistent=n', + *files) + res = [] + for line in log.splitlines(): + line = line.split(':') + if len(line) < 3: + continue + res.append(Error(line[0].replace('\\', '/'), + line[1], "", line[2].strip())) + return res + + +async def flake8(files): + """Exec flake8.""" + _, log = await async_exec('flake8', '--doctests', *files) + res = [] + for line in log.splitlines(): + line = line.split(':') + if len(line) < 4: + continue + res.append(Error(line[0].replace('\\', '/'), + line[1], line[2], line[3].strip())) + return res + + +async def lint(files): + """Perform lint.""" + files = [file for file in files if os.path.isfile(file)] + fres, pres = await asyncio.gather(flake8(files), pylint(files)) + + res = fres + pres + res.sort(key=lambda item: item.file) + if res: + print("Pylint & Flake8 errors:") + else: + printc(PASS, "Pylint and Flake8 passed") + + lint_ok = True + for err in res: + err_msg = "{} {}:{} {}".format(err.file, err.line, err.col, err.msg) + + # tests/* does not have to pass lint + if err.file.startswith('tests/'): + print(err_msg) + else: + printc(FAIL, err_msg) + lint_ok = False + + return lint_ok + + +async def main(): + """The main loop.""" + # Ensure we are in the homeassistant root + os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + + files = await git() + if not files: + print("No changed files found. Please ensure you have added your " + "changes with git add & git commit") + return + + pyfile = re.compile(r".+\.py$") + pyfiles = [file for file in files if pyfile.match(file)] + + print("=============================") + printc('bold', "CHANGED FILES:\n", '\n '.join(pyfiles)) + print("=============================") + + skip_lint = len(sys.argv) > 1 and sys.argv[1] == '--skiplint' + if skip_lint: + printc(FAIL, "LINT DISABLED") + elif not await lint(pyfiles): + printc(FAIL, "Please fix your lint issues before continuing") + return + + test_files = set() + gen_req = False + for fname in pyfiles: + if fname.startswith('homeassistant/components/'): + gen_req = True # requirements script for components + # Find test files... + if fname.startswith('tests/'): + if '/test_' in fname and os.path.isfile(fname): + # All test helpers should be excluded + test_files.add(fname) + else: + parts = fname.split('/') + parts[0] = 'tests' + if parts[-1] == '__init__.py': + parts[-1] = 'test_init.py' + elif parts[-1] == '__main__.py': + parts[-1] = 'test_main.py' + else: + parts[-1] = 'test_' + parts[-1] + fname = '/'.join(parts) + if os.path.isfile(fname): + test_files.add(fname) + + if gen_req: + print("=============================") + if validate_requirements_ok(): + printc(PASS, "script/gen_requirements.py passed") + else: + printc(FAIL, "Please run script/gen_requirements.py") + return + + print("=============================") + if not test_files: + print("No test files identified, ideally you should run tox") + return + + code, _ = await async_exec( + 'pytest', '-vv', '--force-sugar', '--', *test_files, display=True) + print("=============================") + + if code == 0: + printc(PASS, "Yay! This will most likely pass tox") + else: + printc(FAIL, "Tests not passing") + + if skip_lint: + printc(FAIL, "LINT DISABLED") + + +if __name__ == '__main__': + LOOP = asyncio.ProactorEventLoop() if sys.platform == 'win32' \ + else asyncio.get_event_loop() + + try: + LOOP.run_until_complete(main()) + except (FileNotFoundError, KeyboardInterrupt): + pass + finally: + LOOP.close() diff --git a/script/lint b/script/lint index bfce996788e..dc6884f4882 100755 --- a/script/lint +++ b/script/lint @@ -3,25 +3,21 @@ cd "$(dirname "$0")/.." -if [ "$1" = "--all" ]; then - tox -e lint -else - export files="`git diff upstream/dev... --name-only | grep -e '\.py$'`" - echo "=================================================" - echo "FILES CHANGED (git diff upstream/dev... --name-only)" - echo "=================================================" - if [ -z "$files" ] ; then - echo "No python file changed" - exit - fi - printf "%s\n" $files - echo "================" - echo "LINT with flake8" - echo "================" - flake8 --doctests $files - echo "================" - echo "LINT with pylint" - echo "================" - pylint $files - echo +export files="$(git diff $(git merge-base upstream/dev HEAD) --diff-filter=d --name-only | grep -e '\.py$')" +echo '=================================================' +echo '= FILES CHANGED =' +echo '=================================================' +if [ -z "$files" ] ; then + echo "No python file changed. Rather use: tox -e lint" + exit fi +printf "%s\n" $files +echo "================" +echo "LINT with flake8" +echo "================" +flake8 --doctests $files +echo "================" +echo "LINT with pylint" +echo "================" +pylint $(echo "$files" | grep -v '^tests.*') +echo diff --git a/script/release b/script/release index 65a6339cedc..dc3e208bc1a 100755 --- a/script/release +++ b/script/release @@ -21,10 +21,11 @@ fi CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` -if [ "$CURRENT_BRANCH" != "master" ] +if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "rc" ] then - echo "You have to be on the master branch to release." + echo "You have to be on the master or rc branch to release." exit 1 fi -python3 setup.py sdist bdist_wheel upload +python3 setup.py sdist bdist_wheel +python3 -m twine upload dist/* --skip-existing diff --git a/script/translations_develop b/script/translations_develop new file mode 100755 index 00000000000..eb9d685fa8e --- /dev/null +++ b/script/translations_develop @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Compile the current translation strings files for testing + +# Safe bash settings +# -e Exit on command fail +# -u Exit on unset variable +# -o pipefail Exit if piped command has error code +set -eu -o pipefail + +cd "$(dirname "$0")/.." + +mkdir -p build/translations-download + +script/translations_upload_merge.py + +# Use the generated translations upload file as the mock output from the +# Lokalise download +mv build/translations-upload.json build/translations-download/en.json + +script/translations_download_split.py diff --git a/script/version_bump.py b/script/version_bump.py new file mode 100755 index 00000000000..59060a7075b --- /dev/null +++ b/script/version_bump.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Helper script to bump the current version.""" +import argparse +import re + +from packaging.version import Version + +from homeassistant import const + + +def _bump_release(release, bump_type): + """Bump a release tuple consisting of 3 numbers.""" + major, minor, patch = release + + if bump_type == 'patch': + patch += 1 + elif bump_type == 'minor': + minor += 1 + patch = 0 + + return major, minor, patch + + +def bump_version(version, bump_type): + """Return a new version given a current version and action.""" + to_change = {} + + if bump_type == 'minor': + # Convert 0.67.3 to 0.68.0 + # Convert 0.67.3.b5 to 0.68.0 + # Convert 0.67.3.dev0 to 0.68.0 + # Convert 0.67.0.b5 to 0.67.0 + # Convert 0.67.0.dev0 to 0.67.0 + to_change['dev'] = None + to_change['pre'] = None + + if not version.is_prerelease or version.release[2] != 0: + to_change['release'] = _bump_release(version.release, 'minor') + + elif bump_type == 'patch': + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.b5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + to_change['dev'] = None + to_change['pre'] = None + + if not version.is_prerelease: + to_change['release'] = _bump_release(version.release, 'patch') + + elif bump_type == 'dev': + # Convert 0.67.3 to 0.67.4.dev0 + # Convert 0.67.3.b5 to 0.67.4.dev0 + # Convert 0.67.3.dev0 to 0.67.3.dev1 + if version.is_devrelease: + to_change['dev'] = ('dev', version.dev + 1) + else: + to_change['pre'] = ('dev', 0) + to_change['release'] = _bump_release(version.release, 'minor') + + elif bump_type == 'beta': + # Convert 0.67.5 to 0.67.6b0 + # Convert 0.67.0.dev0 to 0.67.0b0 + # Convert 0.67.5.b4 to 0.67.5b5 + + if version.is_devrelease: + to_change['dev'] = None + to_change['pre'] = ('b', 0) + + elif version.is_prerelease: + if version.pre[0] == 'a': + to_change['pre'] = ('b', 0) + if version.pre[0] == 'b': + to_change['pre'] = ('b', version.pre[1] + 1) + else: + to_change['pre'] = ('b', 0) + to_change['release'] = _bump_release(version.release, 'patch') + + else: + to_change['release'] = _bump_release(version.release, 'patch') + to_change['pre'] = ('b', 0) + + else: + assert False, 'Unsupported type: {}'.format(bump_type) + + temp = Version('0') + temp._version = version._version._replace(**to_change) + return Version(str(temp)) + + +def write_version(version): + """Update Home Assistant constant file with new version.""" + with open('homeassistant/const.py') as fil: + content = fil.read() + + major, minor, patch = str(version).split('.', 2) + + content = re.sub('MAJOR_VERSION = .*\n', + 'MAJOR_VERSION = {}\n'.format(major), + content) + content = re.sub('MINOR_VERSION = .*\n', + 'MINOR_VERSION = {}\n'.format(minor), + content) + content = re.sub('PATCH_VERSION = .*\n', + "PATCH_VERSION = '{}'\n".format(patch), + content) + + with open('homeassistant/const.py', 'wt') as fil: + content = fil.write(content) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description="Bump version of Home Assistant") + parser.add_argument( + 'type', + help="The type of the bump the version to.", + choices=['beta', 'dev', 'patch', 'minor'], + ) + arguments = parser.parse_args() + current = Version(const.__version__) + bumped = bump_version(current, arguments.type) + assert bumped > current, 'BUG! New version is not newer than old version' + write_version(bumped) + + +def test_bump_version(): + """Make sure it all works.""" + assert bump_version(Version('0.56.0'), 'beta') == Version('0.56.1b0') + assert bump_version(Version('0.56.0b3'), 'beta') == Version('0.56.0b4') + assert bump_version(Version('0.56.0.dev0'), 'beta') == Version('0.56.0b0') + + assert bump_version(Version('0.56.3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0b3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0.dev0'), 'dev') == \ + Version('0.56.0.dev1') + + assert bump_version(Version('0.56.3'), 'patch') == \ + Version('0.56.4') + assert bump_version(Version('0.56.3.b3'), 'patch') == \ + Version('0.56.3') + assert bump_version(Version('0.56.0.dev0'), 'patch') == \ + Version('0.56.0') + + assert bump_version(Version('0.56.0'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.b3'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.3.b3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.dev0'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.2.dev0'), 'minor') == \ + Version('0.57.0') + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 024b2df3b38..a317aeb18f1 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ from setuptools import setup, find_packages import homeassistant.const as hass_const - PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' @@ -50,16 +49,14 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.6', - 'async_timeout==2.0.0', - 'astral==1.5', + 'aiohttp==3.0.9', + 'async_timeout==2.0.1', + 'astral==1.6', 'certifi>=2017.4.17', 'attrs==17.4.0', ] -MIN_PY_VERSION = '.'.join(map( - str, - hass_const.REQUIRED_PYTHON_VER)) +MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( name=PROJECT_PACKAGE_NAME, diff --git a/tests/common.py b/tests/common.py index 15ce80a9552..bc84b3493a8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -24,7 +24,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) from homeassistant.components import mqtt, recorder -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) _TEST_INSTANCE_PORT = SERVER_PORT diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index d9f0c8e156d..d7871e82afc 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -21,7 +21,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -49,7 +49,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _flash_briefing_req(client, briefing_id): diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 2c8fafde155..d15c7ccbb34 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -95,7 +95,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _intent_req(client, data=None): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8de4d0d9aff..8199652d09e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -950,42 +950,6 @@ def test_api_set_color_rgb(hass): assert msg['header']['name'] == 'Response' -@asyncio.coroutine -def test_api_set_color_xy(hass): - """Test api set color process.""" - request = get_new_request( - 'Alexa.ColorController', 'SetColor', 'light#test') - - # add payload - request['directive']['payload']['color'] = { - 'hue': '120', - 'saturation': '0.612', - 'brightness': '0.342', - } - - # setup test devices - hass.states.async_set( - 'light.test', 'off', { - 'friendly_name': "Test light", - 'supported_features': 64, - }) - - call_light = async_mock_service(hass, 'light', 'turn_on') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_light) == 1 - assert call_light[0].data['entity_id'] == 'light.test' - assert call_light[0].data['xy_color'] == (0.23, 0.585) - assert call_light[0].data['brightness'] == 18 - assert msg['header']['name'] == 'Response' - - @asyncio.coroutine def test_api_set_color_temperature(hass): """Test api set color temperature process.""" @@ -1199,10 +1163,10 @@ def test_unsupported_domain(hass): @asyncio.coroutine -def do_http_discovery(config, hass, test_client): +def do_http_discovery(config, hass, aiohttp_client): """Submit a request to the Smart Home HTTP API.""" yield from async_setup_component(hass, alexa.DOMAIN, config) - http_client = yield from test_client(hass.http.app) + http_client = yield from aiohttp_client(hass.http.app) request = get_new_request('Alexa.Discovery', 'Discover') response = yield from http_client.post( @@ -1213,7 +1177,7 @@ def do_http_discovery(config, hass, test_client): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """With `smart_home:` HTTP API is exposed.""" config = { 'alexa': { @@ -1221,7 +1185,7 @@ def test_http_api(hass, test_client): } } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) response_data = yield from response.json() # Here we're testing just the HTTP view glue -- details of discovery are @@ -1230,12 +1194,12 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_disabled(hass, test_client): +def test_http_api_disabled(hass, aiohttp_client): """Without `smart_home:`, the HTTP API is disabled.""" config = { 'alexa': {} } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) assert response.status == 404 diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index c47f23bf902..18c095f4bc1 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -9,7 +9,7 @@ from homeassistant import setup from homeassistant.components.binary_sensor import template from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_hlpr -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from tests.common import ( diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index e44e5cfc1f0..11dd0cb9635 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -105,6 +105,20 @@ LOCATION:Hamburg DESCRIPTION:What a day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:7 +DTSTART;TZID=America/Los_Angeles:20171127T083000 +DTSTAMP:20180301T020053Z +DTEND;TZID=America/Los_Angeles:20171127T093000 +SUMMARY:Enjoy the sun +LOCATION:San Francisco +DESCRIPTION:Sunny day +END:VEVENT +END:VCALENDAR """ ] @@ -225,7 +239,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): }, _add_device) - @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 45)) def test_ongoing_event(self, mock_now): """Test that the ongoing event is returned.""" cal = caldav.WebDavCalendarEventDevice(self.hass, @@ -244,6 +258,44 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "description": "Surprisingly rainy" }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + def test_just_ended_event(self, mock_now): + """Test that the next ongoing event is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "This is a normal event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 17:00:00", + "end_time": "2017-11-27 18:00:00", + "location": "Hamburg", + "description": "Surprisingly rainy" + }) + + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00)) + def test_ongoing_event_different_tz(self, mock_now): + """Test that the ongoing event with another timezone is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "Enjoy the sun", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 16:30:00", + "description": "Sunny day", + "end_time": "2017-11-27 17:30:00", + "location": "San Francisco" + }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30)) def test_ongoing_event_with_offset(self, mock_now): """Test that the offset is taken into account.""" diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 62c8ea8854f..9f94ea9f44c 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -11,7 +11,7 @@ import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, MockDependency TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -421,3 +421,16 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'location': event['location'], 'description': event['description'] }) + + @MockDependency("httplib2") + def test_update_false(self, mock_httplib2): + """Test that the update returns False upon Error.""" + mock_service = Mock() + mock_service.get = Mock( + side_effect=mock_httplib2.ServerNotFoundError("unit test")) + + cal = calendar.GoogleCalendarEventDevice(self.hass, mock_service, None, + {'name': "test"}) + result = cal.data.update() + + self.assertFalse(result) diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index 84eaf107d70..01edca1e996 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -6,7 +6,7 @@ from homeassistant.setup import async_setup_component @asyncio.coroutine -def test_fetching_url(aioclient_mock, hass, test_client): +def test_fetching_url(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com', text='hello world') @@ -19,7 +19,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): 'password': 'pass' }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -33,7 +33,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_limit_refetch(aioclient_mock, hass, test_client): +def test_limit_refetch(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com/5a', text='hello world') aioclient_mock.get('http://example.com/10a', text='hello world') @@ -49,7 +49,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): 'limit_refetch_to_url_change': True, }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -94,7 +94,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_camera_content_type(aioclient_mock, hass, test_client): +def test_camera_content_type(aioclient_mock, hass, aiohttp_client): """Test generic camera with custom content_type.""" svg_image = '' urlsvg = 'https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg' @@ -113,7 +113,7 @@ def test_camera_content_type(aioclient_mock, hass, test_client): yield from async_setup_component(hass, 'camera', { 'camera': [cam_config_svg, cam_config_normal]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp_1 = yield from client.get('/api/camera_proxy/camera.config_test_svg') assert aioclient_mock.call_count == 1 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 87612da9faa..465d6276ad5 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_PICTURE import homeassistant.components.camera as camera import homeassistant.components.http as http from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 42ce7bd7add..1098c8c9233 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -12,7 +12,7 @@ from tests.common import mock_registry @asyncio.coroutine -def test_loading_file(hass, test_client): +def test_loading_file(hass, aiohttp_client): """Test that it loads image from disk.""" mock_registry(hass) @@ -25,7 +25,7 @@ def test_loading_file(hass, test_client): 'file_path': 'mock.file', }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) m_open = MockOpen(read_data=b'hello') with mock.patch( @@ -57,7 +57,7 @@ def test_file_not_readable(hass, caplog): @asyncio.coroutine -def test_camera_content_type(hass, test_client): +def test_camera_content_type(hass, aiohttp_client): """Test local_file camera content_type.""" cam_config_jpg = { 'name': 'test_jpg', @@ -84,7 +84,7 @@ def test_camera_content_type(hass, test_client): 'camera': [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) image = 'hello' m_open = MockOpen(read_data=image.encode()) diff --git a/tests/components/camera/test_mqtt.py b/tests/components/camera/test_mqtt.py index 20d15efd982..d83054d7732 100644 --- a/tests/components/camera/test_mqtt.py +++ b/tests/components/camera/test_mqtt.py @@ -8,7 +8,7 @@ from tests.common import ( @asyncio.coroutine -def test_run_camera_setup(hass, test_client): +def test_run_camera_setup(hass, aiohttp_client): """Test that it fetches the given payload.""" topic = 'test/camera' yield from async_mock_mqtt_component(hass) @@ -24,7 +24,7 @@ def test_run_camera_setup(hass, test_client): async_fire_mqtt_message(hass, topic, 'beer') yield from hass.async_block_till_done() - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get(url) assert resp.status == 200 body = yield from resp.text() diff --git a/tests/components/climate/test_ecobee.py b/tests/components/climate/test_ecobee.py index 4732376fceb..eb843d8eb34 100644 --- a/tests/components/climate/test_ecobee.py +++ b/tests/components/climate/test_ecobee.py @@ -3,6 +3,7 @@ import unittest from unittest import mock import homeassistant.const as const import homeassistant.components.climate.ecobee as ecobee +from homeassistant.components.climate import STATE_OFF class TestEcobee(unittest.TestCase): @@ -23,6 +24,7 @@ class TestEcobee(unittest.TestCase): 'desiredFanMode': 'on'}, 'settings': {'hvacMode': 'auto', 'fanMinOnTime': 10, + 'heatCoolMinDelta': 50, 'holdAction': 'nextTransition'}, 'equipmentStatus': 'fan', 'events': [{'name': 'Event1', @@ -81,17 +83,17 @@ class TestEcobee(unittest.TestCase): def test_desired_fan_mode(self): """Test desired fan mode property.""" - self.assertEqual('on', self.thermostat.desired_fan_mode) + self.assertEqual('on', self.thermostat.current_fan_mode) self.ecobee['runtime']['desiredFanMode'] = 'auto' - self.assertEqual('auto', self.thermostat.desired_fan_mode) + self.assertEqual('auto', self.thermostat.current_fan_mode) def test_fan(self): """Test fan property.""" self.assertEqual(const.STATE_ON, self.thermostat.fan) self.ecobee['equipmentStatus'] = '' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) self.ecobee['equipmentStatus'] = 'heatPump, heatPump2' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) def test_current_hold_mode_away_temporary(self): """Test current hold mode when away.""" @@ -180,7 +182,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) @@ -189,7 +191,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = 'compCool1' @@ -197,7 +199,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'cool'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = '' @@ -205,7 +207,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'idle'}, self.thermostat.device_state_attributes) @@ -214,7 +216,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'Unknown'}, self.thermostat.device_state_attributes) @@ -321,7 +323,7 @@ class TestEcobee(unittest.TestCase): self.assertFalse(self.data.ecobee.delete_vacation.called) self.assertFalse(self.data.ecobee.resume_program.called) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) self.assertFalse(self.data.ecobee.set_climate_hold.called) def test_set_auto_temp_hold(self): @@ -337,21 +339,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temp_hold(30.0) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) # Heat mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 50, 30, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) # Cool mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) def test_set_temperature(self): """Test set temperature.""" @@ -366,21 +368,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 25, 15, 'nextTransition')]) # Cool -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temperature(temperature=20.5) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 0.5, 'nextTransition')]) + [mock.call(1, 20.5, 20.5, 'nextTransition')]) # Heat -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40, 20, 'nextTransition')]) + [mock.call(1, 20, 20, 'nextTransition')]) # Heat -> Auto self.data.reset_mock() @@ -450,3 +452,17 @@ class TestEcobee(unittest.TestCase): """Test climate list property.""" self.assertEqual(['Climate1', 'Climate2'], self.thermostat.climate_list) + + def test_set_fan_mode_on(self): + """Test set fan mode to on.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('on') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'on', 20, 40, 'nextTransition')]) + + def test_set_fan_mode_auto(self): + """Test set fan mode to auto.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('auto') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'auto', 20, 40, 'nextTransition')]) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index abc9e6d74c2..bd0b764c6fe 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant import loader from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.components import climate, input_boolean, switch import homeassistant.components as comps from tests.common import (assert_setup_component, get_test_home_assistant, diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 98ddebb5db3..55c6290c158 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -11,8 +11,11 @@ from homeassistant.components.cloud import DOMAIN, auth_api, iot from tests.common import mock_coro +GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync' + + @pytest.fixture -def cloud_client(hass, test_client): +def cloud_client(hass, aiohttp_client): """Fixture that can fetch from the cloud client.""" with patch('homeassistant.components.cloud.Cloud.async_start', return_value=mock_coro()): @@ -23,12 +26,13 @@ def cloud_client(hass, test_client): 'user_pool_id': 'user_pool_id', 'region': 'region', 'relayer': 'relayer', + 'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL, } })) hass.data['cloud']._decode_claims = \ lambda token: jwt.get_unverified_claims(token) with patch('homeassistant.components.cloud.Cloud.write_user_info'): - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture @@ -38,6 +42,21 @@ def mock_cognito(): yield mock_cog() +async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): + """Test syncing Google Actions.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 200 + + +async def test_google_actions_sync_fails(mock_cognito, cloud_client, + aioclient_mock): + """Test syncing Google Actions gone bad.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 403 + + @asyncio.coroutine def test_account_view_no_account(cloud_client): """Test fetching account if no account available.""" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 70990519a0b..91f8ab8316d 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -29,6 +29,7 @@ def test_constructor_loads_info_from_constant(): 'user_pool_id': 'test-user_pool_id', 'region': 'test-region', 'relayer': 'test-relayer', + 'google_actions_sync_url': 'test-google_actions_sync_url', } }), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', return_value=mock_coro(True)): @@ -43,6 +44,7 @@ def test_constructor_loads_info_from_constant(): assert cl.user_pool_id == 'test-user_pool_id' assert cl.region == 'test-region' assert cl.relayer == 'test-relayer' + assert cl.google_actions_sync_url == 'test-google_actions_sync_url' @asyncio.coroutine diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 1551ba74319..cfe6b12baac 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,11 +17,11 @@ from tests.common import MockConfigEntry, MockModule, mock_coro_func @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(config_entries.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine @@ -101,9 +101,7 @@ def test_initialize_flow(hass, client): schema[vol.Required('password')] = str return self.async_show_form( - title='test-title', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' @@ -121,8 +119,8 @@ def test_initialize_flow(hass, client): assert data == { 'type': 'form', - 'title': 'test-title', - 'description': 'test-description', + 'domain': 'test', + 'step_id': 'init', 'data_schema': [ { 'name': 'username', @@ -157,6 +155,7 @@ def test_abort(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'reason': 'bla', 'type': 'abort' } @@ -186,6 +185,7 @@ def test_create_account(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'title': 'Test Entry', 'type': 'create_entry' } @@ -203,7 +203,6 @@ def test_two_step_flow(hass, client): @asyncio.coroutine def async_step_init(self, user_input=None): return self.async_show_form( - title='test-title', step_id='account', data_schema=vol.Schema({ 'user_title': str @@ -224,8 +223,8 @@ def test_two_step_flow(hass, client): flow_id = data.pop('flow_id') assert data == { 'type': 'form', - 'title': 'test-title', - 'description': None, + 'domain': 'test', + 'step_id': 'account', 'data_schema': [ { 'name': 'user_title', @@ -243,6 +242,7 @@ def test_two_step_flow(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'type': 'create_entry', 'title': 'user-title', } @@ -262,7 +262,6 @@ def test_get_progress_index(hass, client): def async_step_account(self, user_input=None): return self.async_show_form( step_id='account', - title='Finish setup' ) with patch.dict(HANDLERS, {'test': TestFlow}): @@ -292,9 +291,7 @@ def test_get_progress_flow(hass, client): schema[vol.Required('password')] = str return self.async_show_form( - title='test-title', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 4d82d695f8b..5b52b3d5711 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,14 +8,14 @@ from tests.common import mock_coro @asyncio.coroutine -def test_validate_config_ok(hass, test_client): +def test_validate_config_ok(hass, aiohttp_client): """Test checking config.""" with patch.object(config, 'SECTIONS', ['core']): yield from async_setup_component(hass, 'config', {}) yield from asyncio.sleep(0.1, loop=hass.loop) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) with patch( 'homeassistant.components.config.core.async_check_ha_config_file', diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index f12774c25d9..100a18618e6 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -9,12 +9,12 @@ from homeassistant.config import DATA_CUSTOMIZE @asyncio.coroutine -def test_get_entity(hass, test_client): +def test_get_entity(hass, aiohttp_client): """Test getting entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -38,12 +38,12 @@ def test_get_entity(hass, test_client): @asyncio.coroutine -def test_update_entity(hass, test_client): +def test_update_entity(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_entity(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_key(hass, test_client): +def test_update_entity_invalid_key(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/not_entity', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_entity_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_json(hass, test_client): +def test_update_entity_invalid_json(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/hello.beer', data='not json') diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index aa7a5ce5f0e..fd7c6999477 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -8,11 +8,11 @@ from tests.common import mock_registry, MockEntity, MockEntityPlatform @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(entity_registry.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) async def test_get_entity(hass, client): diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index ad28b6eb9b8..06ba2ff1105 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -11,12 +11,12 @@ VIEW_NAME = 'api:config:group:config' @asyncio.coroutine -def test_get_device_config(hass, test_client): +def test_get_device_config(hass, aiohttp_client): """Test getting device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -40,12 +40,12 @@ def test_get_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config(hass, test_client): +def test_update_device_config(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_key(hass, test_client): +def test_update_device_config_invalid_key(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/not a slug', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_device_config_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_data(hass, test_client): +def test_update_device_config_invalid_data(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data=json.dumps({ @@ -121,12 +121,12 @@ def test_update_device_config_invalid_data(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_json(hass, test_client): +def test_update_device_config_invalid_json(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data='not json') diff --git a/tests/components/config/test_hassbian.py b/tests/components/config/test_hassbian.py index 9038ccc6aa4..85fbf0c2e5a 100644 --- a/tests/components/config/test_hassbian.py +++ b/tests/components/config/test_hassbian.py @@ -34,13 +34,13 @@ def test_setup_check_env_works(hass, loop): @asyncio.coroutine -def test_get_suites(hass, test_client): +def test_get_suites(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/config/hassbian/suites') assert resp.status == 200 result = yield from resp.json() @@ -53,13 +53,13 @@ def test_get_suites(hass, test_client): @asyncio.coroutine -def test_install_suite(hass, test_client): +def test_install_suite(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/hassbian/suites/openzwave/install') assert resp.status == 200 diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 2d5d814ac8a..57ea7e7a492 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -17,7 +17,7 @@ def test_config_setup(hass, loop): @asyncio.coroutine -def test_load_on_demand_already_loaded(hass, test_client): +def test_load_on_demand_already_loaded(hass, aiohttp_client): """Test getting suites.""" mock_component(hass, 'zwave') @@ -34,7 +34,7 @@ def test_load_on_demand_already_loaded(hass, test_client): @asyncio.coroutine -def test_load_on_demand_on_load(hass, test_client): +def test_load_on_demand_on_load(hass, aiohttp_client): """Test getting suites.""" with patch.object(config, 'SECTIONS', []), \ patch.object(config, 'ON_DEMAND', ['zwave']): diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index c98385a3c32..672bafeaf28 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -16,12 +16,12 @@ VIEW_NAME = 'api:config:zwave:device_config' @pytest.fixture -def client(loop, hass, test_client): +def client(loop, hass, aiohttp_client): """Client to communicate with Z-Wave config views.""" with patch.object(config, 'SECTIONS', ['zwave']): loop.run_until_complete(async_setup_component(hass, 'config', {})) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/cover/test_group.py b/tests/components/cover/test_group.py new file mode 100644 index 00000000000..288e1c5e047 --- /dev/null +++ b/tests/components/cover/test_group.py @@ -0,0 +1,350 @@ +"""The tests for the group cover platform.""" + +import unittest +from datetime import timedelta +import homeassistant.util.dt as dt_util + +from homeassistant import setup +from homeassistant.components import cover +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, DOMAIN) +from homeassistant.components.cover.group import DEFAULT_NAME +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, STATE_OPEN, STATE_CLOSED) +from tests.common import ( + assert_setup_component, get_test_home_assistant, fire_time_changed) + +COVER_GROUP = 'cover.cover_group' +DEMO_COVER = 'cover.kitchen_window' +DEMO_COVER_POS = 'cover.hall_window' +DEMO_COVER_TILT = 'cover.living_room_window' +DEMO_TILT = 'cover.tilt_demo' + +CONFIG = { + DOMAIN: [ + {'platform': 'demo'}, + {'platform': 'group', + CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]} + ] +} + + +class TestMultiCover(unittest.TestCase): + """Test the group cover platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_attributes(self): + """Test handling of state attributes.""" + config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} + + with assert_setup_component(1, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, config) + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_FRIENDLY_NAME), DEFAULT_NAME) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open / close / stop + self.hass.states.set( + DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 11) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_cover_position + self.hass.states.set( + DEMO_COVER_POS, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 15) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open tilt / close tilt / stop tilt + self.hass.states.set( + DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 127) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_tilt_position + self.hass.states.set( + DEMO_COVER_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 255) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # ### Test assumed state ### + # ########################## + + # For covers + self.hass.states.set( + DEMO_COVER, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 244) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.hass.states.remove(DEMO_COVER) + self.hass.block_till_done() + self.hass.states.remove(DEMO_COVER_POS) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 240) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # For tilts + self.hass.states.set( + DEMO_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 128) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.hass.states.remove(DEMO_COVER_TILT) + self.hass.states.set(DEMO_TILT, STATE_CLOSED) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + self.hass.states.set( + DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + + def test_open_covers(self): + """Test open cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 100) + + def test_close_covers(self): + """Test close cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 0) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 0) + + def test_stop_covers(self): + """Test stop cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 20) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 80) + + def test_set_cover_position(self): + """Test set cover position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_position(self.hass, 50, COVER_GROUP) + self.hass.block_till_done() + for _ in range(4): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 50) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 50) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 50) + + def test_open_tilts(self): + """Test open tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + def test_close_tilts(self): + """Test close tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + def test_stop_tilts(self): + """Test stop tilts function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + def test_set_tilt_positions(self): + """Test set tilt position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_tilt_position(self.hass, 80, COVER_GROUP) + self.hass.block_till_done() + for _ in range(3): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 80) diff --git a/tests/components/device_tracker/test_geofency.py b/tests/components/device_tracker/test_geofency.py index 5def6a217f4..a955dd0cc11 100644 --- a/tests/components/device_tracker/test_geofency.py +++ b/tests/components/device_tracker/test_geofency.py @@ -107,7 +107,7 @@ BEACON_EXIT_CAR = { @pytest.fixture -def geofency_client(loop, hass, test_client): +def geofency_client(loop, hass, aiohttp_client): """Geofency mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -117,7 +117,7 @@ def geofency_client(loop, hass, test_client): }})) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture(autouse=True) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9d122fa17b6..c051983d8fa 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -13,7 +13,7 @@ from homeassistant.core import callback, State from homeassistant.setup import setup_component, async_setup_component from homeassistant.helpers import discovery from homeassistant.loader import get_component -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 2476247e069..90adccf7703 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -19,7 +19,7 @@ def _url(data=None): @pytest.fixture -def locative_client(loop, hass, test_client): +def locative_client(loop, hass, aiohttp_client): """Locative mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -29,7 +29,7 @@ def locative_client(loop, hass, test_client): })) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_meraki.py b/tests/components/device_tracker/test_meraki.py index 74fc577bca8..925ba6d66db 100644 --- a/tests/components/device_tracker/test_meraki.py +++ b/tests/components/device_tracker/test_meraki.py @@ -13,7 +13,7 @@ from homeassistant.components.device_tracker.meraki import URL @pytest.fixture -def meraki_client(loop, hass, test_client): +def meraki_client(loop, hass, aiohttp_client): """Meraki mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -25,7 +25,7 @@ def meraki_client(loop, hass, test_client): } })) - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 2239e13e220..37a3e570b53 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -11,7 +11,7 @@ import homeassistant.components.device_tracker.owntracks as owntracks from homeassistant.setup import setup_component from homeassistant.components import device_tracker from homeassistant.const import CONF_PLATFORM, STATE_NOT_HOME -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe USER = 'greg' DEVICE = 'phone' diff --git a/tests/components/device_tracker/test_owntracks_http.py b/tests/components/device_tracker/test_owntracks_http.py index be8bdd94ecc..d7b48cafe46 100644 --- a/tests/components/device_tracker/test_owntracks_http.py +++ b/tests/components/device_tracker/test_owntracks_http.py @@ -10,7 +10,7 @@ from tests.common import mock_coro, mock_component @pytest.fixture -def mock_client(hass, test_client): +def mock_client(hass, aiohttp_client): """Start the Hass HTTP component.""" mock_component(hass, 'group') mock_component(hass, 'zone') @@ -22,7 +22,7 @@ def mock_client(hass, test_client): 'platform': 'owntracks_http' } })) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index 396d2b88b19..e45d70bc172 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -10,7 +10,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_HOST) from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.device_tracker.upc_connect as platform -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 91988a76212..1617f327d27 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -118,7 +118,7 @@ def hass_hue(loop, hass): @pytest.fixture -def hue_client(loop, hass_hue, test_client): +def hue_client(loop, hass_hue, aiohttp_client): """Create web client for emulated hue api.""" web_app = hass_hue.http.app config = Config(None, { @@ -135,7 +135,7 @@ def hue_client(loop, hass_hue, test_client): HueOneLightStateView(config).register(web_app.router) HueOneLightChangeView(config).register(web_app.router) - return loop.run_until_complete(test_client(web_app)) + return loop.run_until_complete(aiohttp_client(web_app)) @asyncio.coroutine diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index b3032954431..555802f9a2c 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -9,7 +9,7 @@ from aiohttp.hdrs import CONTENT_TYPE from homeassistant import setup, const, core import homeassistant.components as core_components from homeassistant.components import emulated_hue, http -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_instance_port, get_test_home_assistant diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index cb319b67bb2..d45680d132e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -27,7 +27,7 @@ AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} @pytest.fixture -def assistant_client(loop, hass, test_client): +def assistant_client(loop, hass, aiohttp_client): """Create web client for the Google Assistant API.""" loop.run_until_complete( setup.async_setup_component(hass, 'google_assistant', { @@ -44,7 +44,7 @@ def assistant_client(loop, hass, test_client): } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 0c69e453092..dd9373c782a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -21,7 +21,7 @@ async def test_sync_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -88,7 +88,7 @@ async def test_query_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -97,7 +97,7 @@ async def test_query_message(hass): light2 = DemoLight( None, 'Another Light', state=True, - rgb=[237, 224, 33], + hs_color=(180, 75), ct=400, brightness=78, ) @@ -137,7 +137,7 @@ async def test_query_message(hass): 'online': True, 'brightness': 30, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 4194303, 'temperature': 2500, } }, @@ -197,7 +197,7 @@ async def test_execute(hass): "online": True, 'brightness': 20, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 16773155, 'temperature': 2631, }, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4ffb273662e..e6336e05246 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -361,12 +361,10 @@ async def test_color_spectrum_light(hass): """Test ColorSpectrum trait support for light domain.""" assert not trait.ColorSpectrumTrait.supported(light.DOMAIN, 0) assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_RGB_COLOR) - assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_XY_COLOR) + light.SUPPORT_COLOR) trt = trait.ColorSpectrumTrait(State('light.bla', STATE_ON, { - light.ATTR_RGB_COLOR: [255, 10, 10] + light.ATTR_HS_COLOR: (0, 94), })) assert trt.sync_attributes() == { @@ -375,7 +373,7 @@ async def test_color_spectrum_light(hass): assert trt.query_attributes() == { 'color': { - 'spectrumRGB': 16714250 + 'spectrumRGB': 16715535 } } @@ -399,7 +397,7 @@ async def test_color_spectrum_light(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'light.bla', - light.ATTR_RGB_COLOR: [16, 16, 255] + light.ATTR_HS_COLOR: (240, 93.725), } diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 56d6cbe666e..9f20efc08a5 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -26,7 +26,7 @@ def hassio_env(): @pytest.fixture -def hassio_client(hassio_env, hass, test_client): +def hassio_client(hassio_env, hass, aiohttp_client): """Create mock hassio http client.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', Mock(return_value=mock_coro({"result": "ok"}))), \ @@ -38,7 +38,7 @@ def hassio_client(hassio_env, hass, test_client): 'api_password': API_PASSWORD } })) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py deleted file mode 100644 index 61a60cee2ac..00000000000 --- a/tests/components/homekit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for the homekit component.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6f39a8c792b..4d230b81686 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,166 +2,164 @@ This includes tests for all mock object types. """ - -from unittest.mock import patch - -# pylint: disable=unused-import -from pyhap.loader import get_serv_loader, get_char_loader # noqa F401 +import unittest +from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - set_accessory_info, add_preload_service, override_properties, - HomeAccessory, HomeBridge) + add_preload_service, set_accessory_info, override_properties, + HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - -from tests.mock.homekit import ( - get_patch_paths, mock_preload_service, - MockTypeLoader, MockAccessory, MockService, MockChar) - -PATH_SERV = 'pyhap.loader.get_serv_loader' -PATH_CHAR = 'pyhap.loader.get_char_loader' -PATH_ACC, _ = get_patch_paths() + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) -@patch(PATH_CHAR, return_value=MockTypeLoader('char')) -@patch(PATH_SERV, return_value=MockTypeLoader('service')) -def test_add_preload_service(mock_serv, mock_char): - """Test method add_preload_service. +class TestAccessories(unittest.TestCase): + """Test pyhap adapter methods.""" - The methods 'get_serv_loader' and 'get_char_loader' are mocked. - """ - acc = MockAccessory('Accessory') - serv = add_preload_service(acc, 'TestService', - ['TestChar', 'TestChar2'], - ['TestOptChar', 'TestOptChar2']) + def test_add_preload_service(self): + """Test add_preload_service without additional characteristics.""" + acc = Mock() + serv = add_preload_service(acc, 'AirPurifier') + self.assertEqual(acc.mock_calls, [call.add_service(serv)]) + with self.assertRaises(AssertionError): + serv.get_characteristic('Name') - assert serv.display_name == 'TestService' - assert len(serv.characteristics) == 2 - assert len(serv.opt_characteristics) == 2 + # Test with typo in service name + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifierTypo') - acc.services = [] - serv = add_preload_service(acc, 'TestService') + # Test adding additional characteristic as string + serv = add_preload_service(Mock(), 'AirPurifier', 'Name') + serv.get_characteristic('Name') - assert not serv.characteristics - assert not serv.opt_characteristics + # Test adding additional characteristics as list + serv = add_preload_service(Mock(), 'AirPurifier', + ['Name', 'RotationSpeed']) + serv.get_characteristic('Name') + serv.get_characteristic('RotationSpeed') - acc.services = [] - serv = add_preload_service(acc, 'TestService', - 'TestChar', 'TestOptChar') + # Test adding additional characteristic with typo + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 + def test_set_accessory_info(self): + """Test setting the basic accessory information.""" + # Test HomeAccessory + acc = HomeAccessory() + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - assert serv.characteristics[0].display_name == 'TestChar' - assert serv.opt_characteristics[0].display_name == 'TestOptChar' + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + # Test HomeBridge + acc = HomeBridge(None) + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') -def test_override_properties(): - """Test override of characteristic properties with MockChar.""" - char = MockChar('TestChar') - new_prop = {1: 'Test', 2: 'Demo'} - override_properties(char, new_prop) + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - assert char.properties == new_prop + def test_override_properties(self): + """Test overriding property values.""" + serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed') + char_active = serv.get_characteristic('Active') + char_rotation_speed = serv.get_characteristic('RotationSpeed') -def test_set_accessory_info(): - """Test setting of basic accessory information with MockAccessory.""" - acc = MockAccessory('Accessory') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + self.assertTrue( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 100) - assert len(acc.services) == 1 - serv = acc.services[0] + override_properties(char_active, valid_values={'State': 'On'}) + override_properties(char_rotation_speed, properties={'maxValue': 200}) - assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 4 - chars = serv.characteristics + self.assertFalse( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 200) - assert chars[0].display_name == CHAR_NAME - assert chars[0].value == 'name' - assert chars[1].display_name == CHAR_MODEL - assert chars[1].value == 'model' - assert chars[2].display_name == CHAR_MANUFACTURER - assert chars[2].value == 'manufacturer' - assert chars[3].display_name == CHAR_SERIAL_NUMBER - assert chars[3].value == '0000' + def test_home_accessory(self): + """Test HomeAccessory class.""" + acc = HomeAccessory() + self.assertEqual(acc.display_name, ACCESSORY_NAME) + self.assertEqual(acc.category, 1) # Category.OTHER + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL) + acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2) + self.assertEqual(acc.display_name, 'test_name') + self.assertEqual(acc.category, 3) # Category.FAN + self.assertEqual(acc.aid, 2) + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_accessory(mock_pre_serv): - """Test initializing a HomeAccessory object.""" - acc = HomeAccessory('TestAccessory', 'test.accessory', 'WINDOW') + def test_home_bridge(self): + """Test HomeBridge class.""" + bridge = HomeBridge(None) + self.assertEqual(bridge.display_name, BRIDGE_NAME) + self.assertEqual(bridge.category, 2) # Category.BRIDGE + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + serv = bridge.services[1] # SERV_BRIDGING_STATE + self.assertEqual(serv.display_name, SERV_BRIDGING_STATE) - assert acc.display_name == 'TestAccessory' - assert acc.category == 13 # Category.WINDOW - assert len(acc.services) == 1 + bridge = HomeBridge('hass', 'test_name', 'test_model') + self.assertEqual(bridge.display_name, 'test_name') + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') - serv = acc.services[0] - assert serv.display_name == SERV_ACCESSORY_INFO - char_model = serv.get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.accessory' + # setup_message + bridge.setup_message() + # add_paired_client + with patch('pyhap.accessory.Accessory.add_paired_client') \ + as mock_add_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + bridge.add_paired_client('client_uuid', 'client_public') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_bridge(mock_pre_serv): - """Test initializing a HomeBridge object.""" - bridge = HomeBridge('TestBridge', 'test.bridge', b'123-45-678') + self.assertEqual(mock_add_paired_client.call_args, + call('client_uuid', 'client_public')) + self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) - assert bridge.display_name == 'TestBridge' - assert bridge.pincode == b'123-45-678' - assert len(bridge.services) == 2 + # remove_paired_client + with patch('pyhap.accessory.Accessory.remove_paired_client') \ + as mock_remove_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + bridge.remove_paired_client('client_uuid') - assert bridge.services[0].display_name == SERV_ACCESSORY_INFO - assert bridge.services[1].display_name == SERV_BRIDGING_STATE + self.assertEqual( + mock_remove_paired_client.call_args, call('client_uuid')) + self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass')) - char_model = bridge.services[0].get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.bridge' + def test_home_driver(self): + """Test HomeDriver class.""" + bridge = HomeBridge(None) + ip_adress = '127.0.0.1' + port = 51826 + path = '.homekit.state' + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + HomeDriver(bridge, ip_adress, port, path) -def test_mock_accessory(): - """Test attributes and functions of a MockAccessory.""" - acc = MockAccessory('TestAcc') - serv = MockService('TestServ') - acc.add_service(serv) - - assert acc.display_name == 'TestAcc' - assert len(acc.services) == 1 - - assert acc.get_service('TestServ') == serv - assert acc.get_service('NewServ').display_name == 'NewServ' - assert len(acc.services) == 2 - - -def test_mock_service(): - """Test attributes and functions of a MockService.""" - serv = MockService('TestServ') - char = MockChar('TestChar') - opt_char = MockChar('TestOptChar') - serv.add_characteristic(char) - serv.add_opt_characteristic(opt_char) - - assert serv.display_name == 'TestServ' - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 - - assert serv.get_characteristic('TestChar') == char - assert serv.get_characteristic('TestOptChar') == opt_char - assert serv.get_characteristic('NewChar').display_name == 'NewChar' - assert len(serv.characteristics) == 2 - - -def test_mock_char(): - """Test attributes and functions of a MockChar.""" - def callback_method(value): - """Provide a callback options for 'set_value' method.""" - assert value == 'With callback' - - char = MockChar('TestChar') - char.set_value('Value') - - assert char.display_name == 'TestChar' - assert char.get_value() == 'Value' - - char.setter_callback = callback_method - char.set_value('With callback') + self.assertEqual( + mock_driver.call_args, call(bridge, ip_adress, port, path)) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e49674a7b9..e29ed85b5fc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,57 +1,129 @@ """Package to test the get_accessory method.""" -from unittest.mock import patch, MagicMock +import logging +import unittest +from unittest.mock import patch, Mock from homeassistant.core import State -from homeassistant.components.homekit import ( - TYPES, get_accessory, import_types) +from homeassistant.components.climate import ( + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, + TEMP_CELSIUS, TEMP_FAHRENHEIT) + +_LOGGER = logging.getLogger(__name__) + +CONFIG = {} -def test_import_types(): - """Test if all type files are imported correctly.""" - try: - import_types() - assert True - # pylint: disable=broad-except - except Exception: - assert False - - -def test_component_not_supported(): +def test_get_accessory_invalid_aid(caplog): """Test with unsupported component.""" - state = State('demo.unsupported', STATE_UNKNOWN) - - assert True if get_accessory(None, state) is None else False + assert get_accessory(None, State('light.demo', 'on'), + aid=None, config=None) is None + assert caplog.records[0].levelname == 'WARNING' + assert 'invalid aid' in caplog.records[0].msg -def test_sensor_temperature_celsius(): - """Test temperature sensor with Celsius as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 +def test_not_supported(): + """Test if none is returned if entity isn't supported.""" + assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + is None -# pylint: disable=invalid-name -def test_sensor_temperature_fahrenheit(): - """Test temperature sensor with Fahrenheit as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 +class TestGetAccessories(unittest.TestCase): + """Methods to test the get_accessory method.""" + def setUp(self): + """Setup Mock type.""" + self.mock_type = Mock() -def test_cover_set_position(): - """Test cover with support for set_cover_position.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'Window': mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 + def tearDown(self): + """Test if mock type was called.""" + self.assertTrue(self.mock_type.called) + + def test_sensor_temperature_celsius(self): + """Test temperature sensor with Celsius as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + get_accessory(None, state, 2, {}) + + # pylint: disable=invalid-name + def test_sensor_temperature_fahrenheit(self): + """Test temperature sensor with Fahrenheit as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + get_accessory(None, state, 2, {}) + + def test_sensor_humidity(self): + """Test humidity sensor with % as unit.""" + with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): + state = State('sensor.humidity', '20', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + get_accessory(None, state, 2, {}) + + def test_cover_set_position(self): + """Test cover with support for set_cover_position.""" + with patch.dict(TYPES, {'WindowCovering': self.mock_type}): + state = State('cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}) + get_accessory(None, state, 2, {}) + + def test_alarm_control_panel(self): + """Test alarm control panel.""" + config = {ATTR_CODE: '1234'} + with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): + state = State('alarm_control_panel.test', 'armed') + get_accessory(None, state, 2, config) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[1].get('alarm_code'), '1234') + + def test_climate(self): + """Test climate devices.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto') + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], False) # support_auto + + def test_light(self): + """Test light devices.""" + with patch.dict(TYPES, {'Light': self.mock_type}): + state = State('light.test', 'on') + get_accessory(None, state, 2, {}) + + def test_climate_support_auto(self): + """Test climate devices with support for auto mode.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto', { + ATTR_SUPPORTED_FEATURES: + SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}) + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], True) # support_auto + + def test_switch(self): + """Test switch.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('switch.test', 'on') + get_accessory(None, state, 2, {}) + + def test_remote(self): + """Test remote.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('remote.test', 'on') + get_accessory(None, state, 2, {}) + + def test_input_boolean(self): + """Test input_boolean.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('input_boolean.test', 'on') + get_accessory(None, state, 2, {}) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 58c197e69ec..c6d79545487 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,33 +1,22 @@ """Tests for the HomeKit component.""" - import unittest -from unittest.mock import call, patch, ANY - -import voluptuous as vol - -# pylint: disable=unused-import -from pyhap.accessory_driver import AccessoryDriver # noqa F401 +from unittest.mock import call, patch, ANY, Mock from homeassistant import setup -from homeassistant.core import Event -from homeassistant.components.homekit import ( - CONF_PIN_CODE, HOMEKIT_FILE, HomeKit, valid_pin) +from homeassistant.core import State +from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, + DEFAULT_PORT, SERVICE_HOMEKIT_START) +from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, PATH_HOMEKIT -PATH_ACC, _ = get_patch_paths() IP_ADDRESS = '127.0.0.1' - -CONFIG_MIN = {'homekit': {}} -CONFIG = { - 'homekit': { - CONF_PORT: 11111, - CONF_PIN_CODE: '987-65-432', - } -} +PATH_HOMEKIT = 'homeassistant.components.homekit' class TestHomeKit(unittest.TestCase): @@ -41,75 +30,162 @@ class TestHomeKit(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_validate_pincode(self): - """Test async_setup with invalid config option.""" - schema = vol.Schema(valid_pin) + def test_generate_aid(self): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + self.assertIsInstance(aid, int) + self.assertTrue(aid >= 2 and aid <= 18446744073709551615) - for value in ('', '123-456-78', 'a23-45-678', '12345678', 1234): - with self.assertRaises(vol.MultipleInvalid): - schema(value) - - for value in ('123-45-678', '234-56-789'): - self.assertTrue(schema(value)) + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + self.assertIsNone(generate_aid('demo.entity')) @patch(PATH_HOMEKIT + '.HomeKit') def test_setup_min(self, mock_homekit): - """Test async_setup with minimal config option.""" + """Test async_setup with min config options.""" self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG_MIN)) + self.hass, DOMAIN, {DOMAIN: {}})) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 51826), - call().setup_bridge(b'123-45-678')]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, DEFAULT_PORT, ANY, {}), + call().setup()]) + + # Test auto start enabled mock_homekit.reset_mock() + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + + self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) + + @patch(PATH_HOMEKIT + '.HomeKit') + def test_setup_auto_start_disabled(self, mock_homekit): + """Test async_setup with auto start disabled and test service calls.""" + mock_homekit.return_value = homekit = Mock() + + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + self.assertTrue(setup.setup_component( + self.hass, DOMAIN, config)) self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, - [call().start_driver(ANY)]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, 11111, ANY, {}), + call().setup()]) - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_parameters(self, mock_homekit): - """Test async_setup with full config option.""" - self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG)) + # Test start call with driver stopped. + homekit.reset_mock() + homekit.configure_mock(**{'started': False}) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 11111), - call().setup_bridge(b'987-65-432')]) + self.hass.services.call('homekit', 'start') + self.assertEqual(homekit.mock_calls, [call.start()]) - @patch('pyhap.accessory_driver.AccessoryDriver') - def test_homekit_class(self, mock_acc_driver): - """Test interaction between the HomeKit class and pyhap.""" - with patch(PATH_HOMEKIT + '.accessories.HomeBridge') as mock_bridge: - homekit = HomeKit(self.hass, 51826) - homekit.setup_bridge(b'123-45-678') + # Test start call with driver started. + homekit.reset_mock() + homekit.configure_mock(**{'started': True}) - mock_bridge.reset_mock() - self.hass.states.set('demo.demo1', 'on') - self.hass.states.set('demo.demo2', 'off') + self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) + self.assertEqual(homekit.mock_calls, []) - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc, \ - patch(PATH_HOMEKIT + '.import_types') as mock_import_types, \ + def test_homekit_setup(self): + """Test setup of bridge and driver.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: - mock_get_acc.side_effect = ['TempSensor', 'Window'] mock_ip.return_value = IP_ADDRESS - homekit.start_driver(Event(EVENT_HOMEASSISTANT_START)) + homekit.setup() path = self.hass.config.path(HOMEKIT_FILE) + self.assertTrue(isinstance(homekit.bridge, HomeBridge)) + self.assertEqual(mock_driver.mock_calls, [ + call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) - self.assertEqual(mock_import_types.call_count, 1) - self.assertEqual(mock_get_acc.call_count, 2) - self.assertEqual(mock_bridge.mock_calls, - [call().add_accessory('TempSensor'), - call().add_accessory('Window')]) - self.assertEqual(mock_acc_driver.mock_calls, - [call(homekit.bridge, 51826, IP_ADDRESS, path), - call().start()]) - mock_acc_driver.reset_mock() + # Test if stop listener is setup + self.assertEqual( + self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) - self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - self.hass.block_till_done() + def test_homekit_add_accessory(self): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit.bridge = HomeBridge(self.hass) - self.assertEqual(mock_acc_driver.mock_calls, [call().stop()]) + with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ + as mock_add_acc, \ + patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 363398124, {})) + self.assertFalse(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 294192020, {})) + self.assertTrue(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 429982757, {})) + self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) + + def test_homekit_entity_filter(self): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(self.hass, None, entity_filter, {}) + + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None + + homekit.add_bridge_accessory(State('cover.test', 'open')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('light.demo', 'light')) + self.assertFalse(mock_get_acc.called) + + @patch(PATH_HOMEKIT + '.show_setup_message') + @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') + def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): + """Test HomeKit start method.""" + homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit.bridge = HomeBridge(self.hass) + homekit.driver = Mock() + + self.hass.states.set('light.demo', 'on') + state = self.hass.states.all()[0] + + homekit.start() + + self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) + self.assertEqual(mock_show_setup_msg.mock_calls, [ + call(homekit.bridge, self.hass)]) + self.assertEqual(homekit.driver.mock_calls, [call.start()]) + self.assertTrue(homekit.started) + + # Test start() if already started + homekit.driver.reset_mock() + homekit.start() + self.assertEqual(homekit.driver.mock_calls, []) + + def test_homekit_stop(self): + """Test HomeKit stop method.""" + homekit = HomeKit(None, None, None, None) + homekit.driver = Mock() + + # Test if started = False + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver not started + homekit.started = True + homekit.driver.configure_mock(**{'run_sentinel': None}) + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver is started + homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.stop() + self.assertTrue(homekit.driver.stop.called) diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_sensors.py deleted file mode 100644 index 4698c363503..00000000000 --- a/tests/components/homekit/test_sensors.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Test different accessory types: Sensors.""" -import unittest -from unittest.mock import patch - -from homeassistant.components.homekit.const import PROP_CELSIUS -from homeassistant.components.homekit.sensors import ( - TemperatureSensor, calc_temperature) -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) - -from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('sensors') - - -def test_calc_temperature(): - """Test if temperature in Celsius is calculated correctly.""" - assert calc_temperature(STATE_UNKNOWN) is None - assert calc_temperature('test') is None - - assert calc_temperature('20') == 20 - assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 - - -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding sensors.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - get_patch_paths('sensors') - - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - - def test_temperature(self): - """Test if accessory is updated after state change.""" - temperature_sensor = 'sensor.temperature' - - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = TemperatureSensor(self.hass, temperature_sensor, - 'Temperature') - acc.run() - - self.assertEqual(acc.char_temp.value, 0.0) - self.assertEqual(acc.char_temp.properties, PROP_CELSIUS) - - self.hass.states.set(temperature_sensor, STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - - self.hass.states.set(temperature_sensor, '20', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 20) - - self.hass.states.set(temperature_sensor, '75.2', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 24) diff --git a/tests/components/homekit/test_switches.py b/tests/components/homekit/test_switches.py deleted file mode 100644 index d9f2d6c1d1a..00000000000 --- a/tests/components/homekit/test_switches.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Test different accessory types: Switches.""" -import unittest -from unittest.mock import patch - -from homeassistant.core import callback -from homeassistant.components.homekit.switches import Switch -from homeassistant.const import ATTR_SERVICE, EVENT_CALL_SERVICE - -from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('switches') - - -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] - - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) - - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) - - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - switch = 'switch.testswitch' - - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Switch(self.hass, switch, 'Switch') - acc.run() - - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(switch, 'on') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(switch, 'off') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.set_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual(acc.char_on.value, True) - - acc.char_on.set_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'turn_off') - self.assertEqual(acc.char_on.value, False) diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_type_covers.py similarity index 87% rename from tests/components/homekit/test_covers.py rename to tests/components/homekit/test_type_covers.py index fe0ede5d8fb..45631a76c98 100644 --- a/tests/components/homekit/test_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,19 +1,15 @@ """Test different accessory types: Covers.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION) -from homeassistant.components.homekit.covers import Window +from homeassistant.components.homekit.type_covers import WindowCovering from homeassistant.const import ( STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('covers') class TestHomekitSensors(unittest.TestCase): @@ -39,10 +35,11 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Window(self.hass, window_cover, 'Cover') - acc.run() + acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 14) # WindowCovering self.assertEqual(acc.char_current_position.value, 0) self.assertEqual(acc.char_target_position.value, 0) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py new file mode 100644 index 00000000000..ee1900fd7c5 --- /dev/null +++ b/tests/components/homekit/test_type_lights.py @@ -0,0 +1,144 @@ +"""Test different accessory types: Lights.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_lights import Light +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR) +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +class TestHomekitLights(unittest.TestCase): + """Test class for all accessory types regarding lights.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_light_basic(self): + """Test light with char state.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 5) # Lightbulb + self.assertEqual(acc.char_on.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + # Set from HomeKit + acc.char_on.set_value(1) + self.hass.block_till_done() + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + + acc.char_on.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + self.hass.states.set(entity_id, STATE_OFF) + self.hass.block_till_done() + + # Remove entity + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_light_brightness(self): + """Test light with brightness.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_brightness.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 100) + + self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 40) + + # Set from HomeKit + acc.char_brightness.set_value(20) + acc.char_on.set_value(1) + self.hass.block_till_done() + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(40) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + def test_light_rgb_color(self): + """Test light with rgb_color.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_hue.value, 0) + self.assertEqual(acc.char_saturation.value, 75) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_hue.value, 260) + self.assertEqual(acc.char_saturation.value, 90) + + # Set from HomeKit + acc.char_hue.set_value(145) + acc.char_saturation.set_value(75) + self.hass.block_till_done() + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) diff --git a/tests/components/homekit/test_security_systems.py b/tests/components/homekit/test_type_security_systems.py similarity index 62% rename from tests/components/homekit/test_security_systems.py rename to tests/components/homekit/test_type_security_systems.py index 4753e86c084..c689a73bac2 100644 --- a/tests/components/homekit/test_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,18 +1,15 @@ """Test different accessory types: Security Systems.""" import unittest -from unittest.mock import patch from homeassistant.core import callback -from homeassistant.components.homekit.security_systems import SecuritySystem +from homeassistant.components.homekit.type_security_systems import ( + SecuritySystem) from homeassistant.const import ( - ATTR_SERVICE, EVENT_CALL_SERVICE, + ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('security_systems') class TestHomekitSecuritySystems(unittest.TestCase): @@ -36,12 +33,14 @@ class TestHomekitSecuritySystems(unittest.TestCase): def test_switch_set_state(self): """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.testsecurity' + acp = 'alarm_control_panel.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = SecuritySystem(self.hass, acp, 'SecuritySystem') - acc.run() + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code='1234', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 11) # AlarmSystem self.assertEqual(acc.char_current_state.value, 3) self.assertEqual(acc.char_target_state.value, 3) @@ -66,27 +65,56 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 3) + # Set from HomeKit acc.char_target_state.set_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 0) acc.char_target_state.set_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 1) acc.char_target_state.set_value(2) self.hass.block_till_done() self.assertEqual( self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 2) acc.char_target_state.set_value(3) self.hass.block_till_done() self.assertEqual( self.events[3].data[ATTR_SERVICE], 'alarm_disarm') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) + + def test_no_alarm_code(self): + """Test accessory if security_system doesn't require a alarm_code.""" + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code=None, aid=2) + acc.run() + + # Set from HomeKit + acc.char_target_state.set_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py new file mode 100644 index 00000000000..c04c250613d --- /dev/null +++ b/tests/components/homekit/test_type_sensors.py @@ -0,0 +1,70 @@ +"""Test different accessory types: Sensors.""" +import unittest + +from homeassistant.components.homekit.const import PROP_CELSIUS +from homeassistant.components.homekit.type_sensors import ( + TemperatureSensor, HumiditySensor) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + +from tests.common import get_test_home_assistant + + +class TestHomekitSensors(unittest.TestCase): + """Test class for all accessory types regarding sensors.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_temperature(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.temperature' + + acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_temp.value, 0.0) + for key, value in PROP_CELSIUS.items(): + self.assertEqual(acc.char_temp.properties[key], value) + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + self.hass.states.set(entity_id, '20', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_temp.value, 20) + + self.hass.states.set(entity_id, '75.2', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_temp.value, 24) + + def test_humidity(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' + + acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_humidity.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + self.assertEqual(acc.char_humidity.value, 20) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py new file mode 100644 index 00000000000..21d7583152e --- /dev/null +++ b/tests/components/homekit/test_type_switches.py @@ -0,0 +1,104 @@ +"""Test different accessory types: Switches.""" +import unittest + +from homeassistant.core import callback, split_entity_id +from homeassistant.components.homekit.type_switches import Switch +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, + SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) + +from tests.common import get_test_home_assistant + + +class TestHomekitSwitches(unittest.TestCase): + """Test class for all accessory types regarding switches.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_switch_set_state(self): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'switch.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 8) # Switch + + self.assertEqual(acc.char_on.value, False) + + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, True) + + self.hass.states.set(entity_id, STATE_OFF) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + def test_remote_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'remote.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) + + def test_input_boolean_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'input_boolean.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) diff --git a/tests/components/homekit/test_thermostats.py b/tests/components/homekit/test_type_thermostats.py similarity index 56% rename from tests/components/homekit/test_thermostats.py rename to tests/components/homekit/test_type_thermostats.py index fabffe881bb..011fe73377d 100644 --- a/tests/components/homekit/test_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,21 +1,18 @@ """Test different accessory types: Thermostats.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - ATTR_OPERATION_MODE, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.thermostats import Thermostat, STATE_OFF + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) +from homeassistant.components.homekit.type_thermostats import ( + Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('thermostats') class TestHomekitThermostats(unittest.TestCase): @@ -39,12 +36,13 @@ class TestHomekitThermostats(unittest.TestCase): def test_default_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Thermostat(self.hass, climate, 'Climate', False) - acc.run() + acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 9) # Thermostat self.assertEqual(acc.char_current_heat_cool.value, 0) self.assertEqual(acc.char_target_heat_cool.value, 0) @@ -78,6 +76,30 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 23.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 19.0) + self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_OFF, {ATTR_OPERATION_MODE: STATE_OFF, ATTR_TEMPERATURE: 22.0, @@ -90,6 +112,45 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 18.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 18.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit acc.char_target_temp.set_value(19.0) self.hass.block_till_done() @@ -110,7 +171,7 @@ class TestHomekitThermostats(unittest.TestCase): def test_auto_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' acc = Thermostat(self.hass, climate, 'Climate', True) acc.run() @@ -177,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + + def test_thermostat_fahrenheit(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', True) + acc.run() + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) + self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) + self.assertEqual(acc.char_current_temp.value, 23.0) + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 1) + + # Set from HomeKit + acc.char_cooling_thresh_temp.set_value(23) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + + acc.char_heating_thresh_temp.set_value(22) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + + acc.char_target_temp.set_value(24.0) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py new file mode 100644 index 00000000000..d6ef5856f85 --- /dev/null +++ b/tests/components/homekit/test_util.py @@ -0,0 +1,102 @@ +"""Test HomeKit util module.""" +import unittest + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.components.homekit.util import ( + show_setup_message, dismiss_setup_message, convert_to_float, + temperature_to_homekit, temperature_to_states, ATTR_CODE) +from homeassistant.components.homekit.util import validate_entity_config \ + as vec +from homeassistant.components.persistent_notification import ( + SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) +from homeassistant.const import ( + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +class TestUtil(unittest.TestCase): + """Test all HomeKit util methods.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_validate_entity_config(self): + """Test validate entities.""" + configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + {'demo.test': 'test'}, {'demo.test': [1, 2]}, + {'demo.test': None}] + + for conf in configs: + with self.assertRaises(vol.Invalid): + vec(conf) + + self.assertEqual(vec({}), {}) + self.assertEqual( + vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}), + {'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) + + def test_show_setup_msg(self): + """Test show setup message as persistence notification.""" + bridge = HomeBridge(self.hass) + + show_setup_message(bridge, self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) + + def test_dismiss_setup_msg(self): + """Test dismiss setup message.""" + dismiss_setup_message(self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) + + def test_convert_to_float(self): + """Test convert_to_float method.""" + self.assertEqual(convert_to_float(12), 12) + self.assertEqual(convert_to_float(12.4), 12.4) + self.assertIsNone(convert_to_float(STATE_UNKNOWN)) + self.assertIsNone(convert_to_float(None)) + + def test_temperature_to_homekit(self): + """Test temperature conversion from HA to HomeKit.""" + self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) + self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) + + def test_temperature_to_states(self): + """Test temperature conversion from HomeKit to HA.""" + self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) + self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 604ee9c0c9b..a44d17d513d 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -55,19 +55,19 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_without_password(app, test_client): +async def test_access_without_password(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], None) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert resp.status == 200 -async def test_access_with_password_in_header(app, test_client): +async def test_access_with_password_in_header(app, aiohttp_client): """Test access with password in URL.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -78,10 +78,10 @@ async def test_access_with_password_in_header(app, test_client): assert req.status == 401 -async def test_access_with_password_in_query(app, test_client): +async def test_access_with_password_in_query(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/', params={ 'api_password': API_PASSWORD @@ -97,10 +97,10 @@ async def test_access_with_password_in_query(app, test_client): assert resp.status == 401 -async def test_basic_auth_works(app, test_client): +async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', @@ -125,7 +125,7 @@ async def test_basic_auth_works(app, test_client): assert req.status == 401 -async def test_access_with_trusted_ip(test_client): +async def test_access_with_trusted_ip(aiohttp_client): """Test access with an untrusted ip address.""" app = web.Application() app.router.add_get('/', mock_handler) @@ -133,7 +133,7 @@ async def test_access_with_trusted_ip(test_client): setup_auth(app, TRUSTED_NETWORKS, 'some-pass') set_mock_ip = mock_real_ip(app) - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 2d7885d959f..c5691cf3e2a 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,7 +15,7 @@ from . import mock_real_ip BANNED_IPS = ['200.201.202.203', '100.64.0.2'] -async def test_access_from_banned_ip(hass, test_client): +async def test_access_from_banned_ip(hass, aiohttp_client): """Test accessing to server from banned IP. Both trusted and not.""" app = web.Application() setup_bans(hass, app, 5) @@ -24,7 +24,7 @@ async def test_access_from_banned_ip(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in BANNED_IPS: set_real_ip(remote_addr) @@ -54,7 +54,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, test_client): +async def test_ip_bans_file_creation(hass, aiohttp_client): """Testing if banned IP file created.""" app = web.Application() app['hass'] = hass @@ -70,7 +70,7 @@ async def test_ip_bans_file_creation(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) m = mock_open() diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 50464b36277..27367b4173e 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -47,12 +47,12 @@ async def mock_handler(request): @pytest.fixture -def client(loop, test_client): +def client(loop, aiohttp_client): """Fixture to setup a web.Application.""" app = web.Application() app.router.add_get('/', mock_handler) setup_cors(app, [TRUSTED_ORIGIN]) - return loop.run_until_complete(test_client(app)) + return loop.run_until_complete(aiohttp_client(app)) async def test_cors_requests(client): diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 6cca1af8ccc..2b966daff6c 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -8,7 +8,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -async def get_client(test_client, validator): +async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() app['hass'] = Mock(is_running=True) @@ -24,14 +24,14 @@ async def get_client(test_client, validator): return b'' TestView().register(app.router) - client = await test_client(app) + client = await aiohttp_client(app) return client -async def test_validator(test_client): +async def test_validator(aiohttp_client): """Test the validator.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ vol.Required('test'): str }))) @@ -49,10 +49,10 @@ async def test_validator(test_client): assert resp.status == 400 -async def test_validator_allow_empty(test_client): +async def test_validator_allow_empty(aiohttp_client): """Test the validator with empty data.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ # Although we allow empty, our schema should still be able # to validate an empty dict. vol.Optional('test'): str diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 1dcf45f48c3..c02e203444f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -15,12 +15,13 @@ class TestView(http.HomeAssistantView): return 'hello' -async def test_registering_view_while_running(hass, test_client, unused_port): +async def test_registering_view_while_running(hass, aiohttp_client, + aiohttp_unused_port): """Test that we can register a view while the server is running.""" await async_setup_component( hass, http.DOMAIN, { http.DOMAIN: { - http.CONF_SERVER_PORT: unused_port(), + http.CONF_SERVER_PORT: aiohttp_unused_port(), } } ) @@ -73,17 +74,16 @@ async def test_api_no_base_url(hass): assert hass.config.api.base_url == 'http://127.0.0.1:8123' -async def test_not_log_password(hass, unused_port, test_client, caplog): +async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" result = await async_setup_component(hass, 'api', { 'http': { - http.CONF_SERVER_PORT: unused_port(), http.CONF_API_PASSWORD: 'some-pass' } }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 3e4f9023537..61846eb94c2 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -11,13 +11,13 @@ async def mock_handler(request): return web.Response(text=str(request[KEY_REAL_IP])) -async def test_ignore_x_forwarded_for(test_client): +async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, False) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' @@ -27,13 +27,13 @@ async def test_ignore_x_forwarded_for(test_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(test_client): +async def test_use_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, True) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py new file mode 100644 index 00000000000..ac0e23edd64 --- /dev/null +++ b/tests/components/http/test_view.py @@ -0,0 +1,15 @@ +"""Tests for Home Assistant View.""" +from aiohttp.web_exceptions import HTTPInternalServerError +import pytest + +from homeassistant.components.http.view import HomeAssistantView + + +async def test_invalid_json(caplog): + """Test trying to return invalid JSON.""" + view = HomeAssistantView() + + with pytest.raises(HTTPInternalServerError): + view.json(object) + + assert str(object) in caplog.text diff --git a/tests/components/hue/__init__.py b/tests/components/hue/__init__.py new file mode 100644 index 00000000000..8cff8700aaf --- /dev/null +++ b/tests/components/hue/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hue component.""" diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py new file mode 100644 index 00000000000..7ccc202b31b --- /dev/null +++ b/tests/components/hue/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for Hue tests.""" +from unittest.mock import patch + +import pytest + +from tests.common import mock_coro_func + + +@pytest.fixture +def mock_bridge(): + """Mock the HueBridge from initializing.""" + with patch('homeassistant.components.hue._find_username_from_config', + return_value=None), \ + patch('homeassistant.components.hue.HueBridge') as mock_bridge: + mock_bridge().async_setup = mock_coro_func() + mock_bridge.reset_mock() + yield mock_bridge diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py new file mode 100644 index 00000000000..39351699df5 --- /dev/null +++ b/tests/components/hue/test_bridge.py @@ -0,0 +1,99 @@ +"""Test Hue bridge.""" +import asyncio +from unittest.mock import Mock, patch + +import aiohue +import pytest + +from homeassistant.components import hue + +from tests.common import mock_coro + + +class MockBridge(hue.HueBridge): + """Class that sets default for constructor.""" + + def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf', + username=None, **kwargs): + """Initialize a mock bridge.""" + super().__init__(host, hass, filename, username, **kwargs) + + +@pytest.fixture +def mock_request(): + """Mock configurator.async_request_config.""" + with patch('homeassistant.components.configurator.' + 'async_request_config') as mock_request: + yield mock_request + + +async def test_setup_request_config_button_not_pressed(hass, mock_request): + """Test we request config if link button has not been pressed.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_request_config_invalid_username(hass, mock_request): + """Test we request config if username is no longer whitelisted.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.Unauthorized): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_timeout(hass, mock_request): + """Test we give up when there is a timeout.""" + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 0 + + +async def test_only_create_no_username(hass): + """.""" + with patch('aiohue.Bridge.create_user') as mock_create, \ + patch('aiohue.Bridge.initialize') as mock_init: + await MockBridge(hass, username='bla').async_setup() + + assert len(mock_create.mock_calls) == 0 + assert len(mock_init.mock_calls) == 1 + + +async def test_configurator_callback(hass, mock_request): + """.""" + hass.data[hue.DOMAIN] = {} + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + callback = mock_request.mock_calls[0][1][2] + + mock_init = Mock(return_value=mock_coro()) + mock_create = Mock(return_value=mock_coro()) + + with patch('aiohue.Bridge') as mock_bridge, \ + patch('homeassistant.helpers.discovery.async_load_platform', + return_value=mock_coro()) as mock_load_platform, \ + patch('homeassistant.components.hue.save_json') as mock_save: + inst = mock_bridge() + inst.username = 'mock-user' + inst.create_user = mock_create + inst.initialize = mock_init + await callback(None) + + assert len(mock_create.mock_calls) == 1 + assert len(mock_init.mock_calls) == 1 + assert len(mock_save.mock_calls) == 1 + assert mock_save.mock_calls[0][1][1] == { + '1.2.3.4': { + 'username': 'mock-user' + } + } + assert len(mock_load_platform.mock_calls) == 1 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py new file mode 100644 index 00000000000..959e3c6241b --- /dev/null +++ b/tests/components/hue/test_config_flow.py @@ -0,0 +1,184 @@ +"""Tests for Philips Hue config flow.""" +import asyncio +from unittest.mock import patch + +import aiohue +import pytest +import voluptuous as vol + +from homeassistant.components import hue + +from tests.common import MockConfigEntry, mock_coro + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow .""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + + flow = hue.HueFlowHandler() + flow.hass = hass + await flow.async_step_init() + + with patch('aiohue.Bridge') as mock_bridge: + def mock_constructor(host, websession): + mock_bridge.host = host + return mock_bridge + + mock_bridge.side_effect = mock_constructor + mock_bridge.username = 'username-abc' + mock_bridge.config.name = 'Mock Bridge' + mock_bridge.config.bridgeid = 'bridge-id-1234' + mock_bridge.create_user.return_value = mock_coro() + mock_bridge.initialize.return_value = mock_coro() + + result = await flow.async_step_link(user_input={}) + + assert mock_bridge.host == '1.2.3.4' + assert len(mock_bridge.create_user.mock_calls) == 1 + assert len(mock_bridge.initialize.mock_calls) == 1 + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '1.2.3.4', + 'bridge_id': 'bridge-id-1234', + 'username': 'username-abc' + } + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): + """Test config flow discovers only already configured bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert flow.host == '5.6.7.8' + + +async def test_flow_timeout_discovery(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.discovery.discover_nupnp', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_init() + + assert result['type'] == 'abort' + + +async def test_flow_link_timeout(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_button_not_pressed(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_unknown_host(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.RequestError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py new file mode 100644 index 00000000000..f90f58a50c3 --- /dev/null +++ b/tests/components/hue/test_setup.py @@ -0,0 +1,70 @@ +"""Test Hue setup process.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import hue +from homeassistant.components.discovery import SERVICE_HUE + + +async def test_setup_with_multiple_hosts(hass, mock_bridge): + """Multiple hosts specified in the config file.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: [ + {hue.CONF_HOST: '127.0.0.1'}, + {hue.CONF_HOST: '192.168.1.10'}, + ] + } + }) + + assert len(mock_bridge.mock_calls) == 2 + hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) + assert hosts == ['127.0.0.1', '192.168.1.10'] + + +async def test_bridge_discovered(hass, mock_bridge): + """Bridge discovery.""" + assert await async_setup_component(hass, hue.DOMAIN, {}) + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + + +async def test_bridge_configure_and_discovered(hass, mock_bridge): + """Bridge is in the config file, then we discover it.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '192.168.1.10' + } + } + }) + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + hass.data[hue.DOMAIN] = {'192.168.1.10': {}} + + mock_bridge.reset_mock() + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 0 + + +async def test_setup_no_host(hass, aioclient_mock): + """Check we call discovery if domain specified but no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + + result = await async_setup_component( + hass, hue.DOMAIN, {hue.DOMAIN: {}}) + assert result + + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 8a7d648e6f2..ff984aff221 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -33,19 +33,22 @@ class TestDemoLight(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((.4, .6), state.attributes.get(light.ATTR_XY_COLOR)) + self.assertEqual((0.378, 0.574), state.attributes.get( + light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (76, 95, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( - self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), + self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), white_value=254) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) self.assertEqual( - (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) + (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) + self.assertEqual( + (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 3c94fa2af3e..26b949720d9 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -20,8 +20,7 @@ async def test_default_state(hass): assert state.state == 'unavailable' assert state.attributes['supported_features'] == 0 assert state.attributes.get('brightness') is None - assert state.attributes.get('rgb_color') is None - assert state.attributes.get('xy_color') is None + assert state.attributes.get('hs_color') is None assert state.attributes.get('color_temp') is None assert state.attributes.get('white_value') is None assert state.attributes.get('effect_list') is None @@ -85,61 +84,32 @@ async def test_brightness(hass): assert state.attributes['brightness'] == 100 -async def test_xy_color(hass): - """Test XY reporting.""" - await async_setup_component(hass, 'light', {'light': { - 'platform': 'group', 'entities': ['light.test1', 'light.test2'] - }}) - - hass.states.async_set('light.test1', 'on', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['supported_features'] == 64 - assert state.attributes['xy_color'] == (1.0, 1.0) - - hass.states.async_set('light.test2', 'on', - {'xy_color': (0.5, 0.5), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.75, 0.75) - - hass.states.async_set('light.test1', 'off', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.5, 0.5) - - -async def test_rgb_color(hass): +async def test_color(hass): """Test RGB reporting.""" await async_setup_component(hass, 'light', {'light': { 'platform': 'group', 'entities': ['light.test1', 'light.test2'] }}) hass.states.async_set('light.test1', 'on', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 100), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 16 - assert state.attributes['rgb_color'] == (255, 0, 0) + assert state.attributes['hs_color'] == (0, 100) hass.states.async_set('light.test2', 'on', - {'rgb_color': (255, 255, 255), + {'hs_color': (0, 50), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 127, 127) + assert state.attributes['hs_color'] == (0, 75) hass.states.async_set('light.test1', 'off', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 0), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 255, 255) + assert state.attributes['hs_color'] == (0, 50) async def test_white_value(hass): @@ -413,5 +383,7 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data['entity_id'] = ['light.test1', 'light.test2'] + data.pop('rgb_color') + data.pop('xy_color') mock_call.assert_called_once_with('light', 'turn_on', data, blocking=True) diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 559467d5e9a..d73531b1b9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -1,545 +1,682 @@ """Philips Hue lights platform tests.""" - +import asyncio +from collections import deque import logging -import unittest -import unittest.mock as mock -from unittest.mock import call, MagicMock, patch +from unittest.mock import Mock + +import aiohue +from aiohue.lights import Lights +from aiohue.groups import Groups +import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light - -from tests.common import get_test_home_assistant, MockDependency +from homeassistant.util import color _LOGGER = logging.getLogger(__name__) HUE_LIGHT_NS = 'homeassistant.components.light.hue.' - - -class TestSetup(unittest.TestCase): - """Test the Hue light platform.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - def setup_mocks_for_update_lights(self): - """Set up all mocks for update_lights tests.""" - self.mock_bridge = MagicMock() - self.mock_bridge.bridge_id = 'bridge-id' - self.mock_bridge.allow_hue_groups = False - self.mock_api = MagicMock() - self.mock_bridge.get_api.return_value = self.mock_api - self.mock_add_devices = MagicMock() - - def setup_mocks_for_process_lights(self): - """Set up all mocks for process_lights tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def setup_mocks_for_process_groups(self): - """Set up all mocks for process_groups tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_bridge.get_group.return_value = { - 'name': 'Group 0', 'state': {'any_on': True}} - - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def create_mock_bridge(self, host, allow_hue_groups=True): - """Return a mock HueBridge with reasonable defaults.""" - mock_bridge = MagicMock() - mock_bridge.bridge_id = 'bridge-id' - mock_bridge.host = host - mock_bridge.allow_hue_groups = allow_hue_groups - mock_bridge.lights = {} - mock_bridge.lightgroups = {} - return mock_bridge - - def create_mock_lights(self, lights): - """Return a dict suitable for mocking api.get('lights').""" - mock_bridge_lights = lights - - for info in mock_bridge_lights.values(): - if 'state' not in info: - info['state'] = {'on': False} - - return mock_bridge_lights - - def build_mock_light(self, bridge, light_id, name): - """Return a mock HueLight.""" - light = MagicMock() - light.bridge = bridge - light.light_id = light_id - light.name = name - return light - - def test_setup_platform_no_discovery_info(self): - """Test setup_platform without discovery info.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices) - - mock_add_devices.assert_not_called() - - def test_setup_platform_no_bridge_id(self): - """Test setup_platform without a bridge.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices, {}) - - mock_add_devices.assert_not_called() - - def test_setup_platform_one_bridge(self): - """Test setup_platform with one bridge.""" - mock_bridge = MagicMock() - self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge} - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - mock_update_lights.assert_called_once_with( - self.hass, mock_bridge, mock_add_devices) - - def test_setup_platform_multiple_bridges(self): - """Test setup_platform wuth multiple bridges.""" - mock_bridge = MagicMock() - mock_bridge2 = MagicMock() - self.hass.data[hue.DOMAIN] = { - '10.0.0.1': mock_bridge, - '192.168.0.10': mock_bridge2, +GROUP_RESPONSE = { + "1": { + "name": "Group 1", + "lights": [ + "1", + "2" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 254, + "hue": 10000, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, } - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '192.168.0.10'}) - - mock_update_lights.assert_has_calls([ - call(self.hass, mock_bridge, mock_add_devices), - call(self.hass, mock_bridge2, mock_add_devices), - ]) - - @MockDependency('phue') - def test_update_lights_with_no_lights(self, mock_phue): - """Test the update_lights function when no lights are found.""" - self.setup_mocks_for_update_lights() - - with patch(HUE_LIGHT_NS + 'process_lights', return_value=[]) \ - as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_not_called() - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_some_lights(self, mock_phue): - """Test the update_lights function with some lights.""" - self.setup_mocks_for_update_lights() - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_no_groups(self, mock_phue): - """Test the update_lights function when no groups are found.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_lights_and_groups(self, mock_phue): - """Test the update_lights function with both lights and groups.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - mock_groups = [ - self.build_mock_light(self.mock_bridge, 15, 'and'), - self.build_mock_light(self.mock_bridge, 72, 'groups'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', - return_value=mock_groups) as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - # note that mock_lights has been modified in place and - # now contains both lights and groups - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_two_bridges(self, mock_phue): - """Test the update_lights function with two bridges.""" - self.setup_mocks_for_update_lights() - - mock_bridge_one = self.create_mock_bridge('one', False) - mock_bridge_one_lights = self.create_mock_lights( - {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}}) - - mock_bridge_two = self.create_mock_bridge('two', False) - mock_bridge_two_lights = self.create_mock_lights( - {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) - - with patch('homeassistant.components.light.hue.HueLight.' - 'schedule_update_ha_state'): - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_one_lights - with patch.object(mock_bridge_one, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_one, self.mock_add_devices) - - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_two_lights - with patch.object(mock_bridge_two, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_two, self.mock_add_devices) - - self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2]) - self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3]) - - self.assertEqual(len(self.mock_add_devices.mock_calls), 2) - - # first call - name, args, kwargs = self.mock_add_devices.mock_calls[0] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - # second call works the same - name, args, kwargs = self.mock_add_devices.mock_calls[1] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - def test_process_lights_api_error(self): - """Test the process_lights function when the bridge errors out.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = None - - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lights, {}) - - def test_process_lights_no_lights(self): - """Test the process_lights function when bridge returns no lights.""" - self.setup_mocks_for_process_lights() - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lights, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_some_lights(self, mock_hue_light): - """Test the process_lights function with multiple groups.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lights), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_new_light(self, mock_hue_light): - """ - Test the process_lights function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lights = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lights), 2) - - def test_process_groups_api_error(self): - """Test the process_groups function when the bridge errors out.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = None - - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lightgroups, {}) - - def test_process_groups_no_state(self): - """Test the process_groups function when bridge returns no status.""" - self.setup_mocks_for_process_groups() - self.mock_bridge.get_group.return_value = {'name': 'Group 0'} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lightgroups, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_some_groups(self, mock_hue_light): - """Test the process_groups function with multiple groups.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lightgroups), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_new_group(self, mock_hue_light): - """ - Test the process_groups function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lightgroups = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lightgroups), 2) + }, + "2": { + "name": "Group 2", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } +} +LIGHT_1_ON = { + "state": { + "on": True, + "bri": 144, + "hue": 13088, + "sat": 212, + "xy": [0.5128, 0.4147], + "ct": 467, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_1_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_2_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_2_ON = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_RESPONSE = { + "1": LIGHT_1_ON, + "2": LIGHT_2_OFF, +} -class TestHueLight(unittest.TestCase): - """Test the HueLight class.""" +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + bridge = Mock(available=True, allow_groups=False, host='1.1.1.1') + bridge.mock_requests = [] + # We're using a deque so we can schedule multiple responses + # and also means that `popleft()` will blow up if we get more updates + # than expected. + bridge.mock_light_responses = deque() + bridge.mock_group_responses = deque() - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False + async def mock_request(method, path, **kwargs): + kwargs['method'] = method + kwargs['path'] = path + bridge.mock_requests.append(kwargs) - self.light_id = 42 - self.mock_info = MagicMock() - self.mock_bridge = MagicMock() - self.mock_update_lights = MagicMock() - self.mock_allow_unreachable = MagicMock() - self.mock_is_group = MagicMock() - self.mock_allow_in_emulated_hue = MagicMock() - self.mock_is_group = False + if path == 'lights': + return bridge.mock_light_responses.popleft() + elif path == 'groups': + return bridge.mock_group_responses.popleft() + return None - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() + bridge.api.config.apiversion = '9.9.9' + bridge.api.lights = Lights({}, mock_request) + bridge.api.groups = Groups({}, mock_request) - def buildLight( - self, light_id=None, info=None, update_lights=None, is_group=None): - """Helper to build a HueLight object with minimal fuss.""" - if 'state' not in info: - on_key = 'any_on' if is_group is not None else 'on' - info['state'] = {on_key: False} + return bridge - return hue_light.HueLight( - light_id if light_id is not None else self.light_id, - info if info is not None else self.mock_info, - self.mock_bridge, - (update_lights - if update_lights is not None - else self.mock_update_lights), - self.mock_allow_unreachable, self.mock_allow_in_emulated_hue, - is_group if is_group is not None else self.mock_is_group) - def test_unique_id_for_light(self): - """Test the unique_id method with lights.""" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEqual('foobar', light.unique_id) +async def setup_bridge(hass, mock_bridge): + """Load the Hue light platform with the provided bridge.""" + hass.config.components.add(hue.DOMAIN) + hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} + await hass.helpers.discovery.async_load_platform('light', 'hue', { + 'host': 'mock-host' + }) + await hass.async_block_till_done() - light = self.buildLight(info={}) - self.assertIsNone(light.unique_id) - def test_unique_id_for_group(self): - """Test the unique_id method with groups.""" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEqual('foobar', light.unique_id) +async def test_not_load_groups_if_old_bridge(hass, mock_bridge): + """Test that we don't try to load gorups if bridge runs old software.""" + mock_bridge.api.config.apiversion = '1.12.0' + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 0 - light = self.buildLight(info={}, is_group=True) - self.assertIsNone(light.unique_id) + +async def test_no_lights_or_groups(hass, mock_bridge): + """Test the update_lights function when no lights are found.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append({}) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 0 + + +async def test_lights(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + # 1 All Lights group, 2 lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.state == 'off' + + +async def test_lights_color_mode(hass, mock_bridge): + """Test that lights only report appropriate color mode.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert 'color_temp' not in lamp_1.attributes + + new_light1_on = LIGHT_1_ON.copy() + new_light1_on['state'] = new_light1_on['state'].copy() + new_light1_on['state']['colormode'] = 'ct' + mock_bridge.mock_light_responses.append({ + "1": new_light1_on, + }) + mock_bridge.mock_group_responses.append({}) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['color_temp'] == 467 + assert 'hs_color' not in lamp_1.attributes + + +async def test_groups(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + # 1 all lights group, 2 hue group lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.group_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 254 + assert lamp_1.attributes['color_temp'] == 250 + + lamp_2 = hass.states.get('light.group_2') + assert lamp_2 is not None + assert lamp_2.state == 'on' + + +async def test_new_group_discovered(hass, mock_bridge): + """Test if 2nd update has a new group.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + new_group_response = dict(GROUP_RESPONSE) + new_group_response['3'] = { + "name": "Group 3", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(new_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 4 + + new_group = hass.states.get('light.group_3') + assert new_group is not None + assert new_group.state == 'on' + assert new_group.attributes['brightness'] == 153 + assert new_group.attributes['color_temp'] == 250 + + +async def test_new_light_discovered(hass, mock_bridge): + """Test if 2nd update has a new light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + new_light_response = dict(LIGHT_RESPONSE) + new_light_response['3'] = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 3", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "789", + } + + mock_bridge.mock_light_responses.append(new_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + light = hass.states.get('light.hue_lamp_3') + assert light is not None + assert light.state == 'off' + + +async def test_other_group_update(hass, mock_bridge): + """Test changing one group that will impact the state of other light.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2' + assert group_2.state == 'on' + assert group_2.attributes['brightness'] == 153 + assert group_2.attributes['color_temp'] == 250 + + updated_group_response = dict(GROUP_RESPONSE) + updated_group_response['2'] = { + "name": "Group 2 new", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0, + 0 + ], + "ct": 0, + "alert": "none", + "colormode": "ct" + }, + "state": { + "any_on": False, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(updated_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2 new' + assert group_2.state == 'off' + + +async def test_other_light_update(hass, mock_bridge): + """Test changing one light that will impact state of other light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2' + assert lamp_2.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", + } + + mock_bridge.mock_light_responses.append(updated_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2 new' + assert lamp_2.state == 'on' + assert lamp_2.attributes['brightness'] == 100 + + +async def test_update_timeout(hass, mock_bridge): + """Test bridge marked as not available if timeout error during update.""" + mock_bridge.api.lights.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge.api.groups.update = Mock(side_effect=asyncio.TimeoutError) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_update_unauthorized(hass, mock_bridge): + """Test bridge marked as not available if unauthorized during update.""" + mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) + mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_light_turn_on_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = LIGHT_2_ON + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2', + 'brightness': 100, + 'color_temp': 300, + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'bri': 100, + 'on': True, + 'ct': 300, + 'effect': 'none', + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'on' + + +async def test_light_turn_off_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'on' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['1'] = LIGHT_1_OFF + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.hue_lamp_1', + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'on': False, + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'off' def test_available(): """Test available property.""" light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is False light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=True, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=True), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=True, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True + + +def test_hs_color(): + """Test hs_color property.""" + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': None, + 'sat': 123, + 'xy': [0.4, 0.5] + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index d35321b4479..4e8fad261bd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -188,23 +188,25 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_TRANSITION: 10, - light.ATTR_BRIGHTNESS: 20, - light.ATTR_RGB_COLOR: (0, 0, 255)}, - data) + self.assertEqual({ + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_HS_COLOR: (240, 100), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 255}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (0, 0), + light.ATTR_WHITE_VALUE: 255, + }, data) _, data = dev3.last_call('turn_on') - self.assertEqual({light.ATTR_XY_COLOR: (.4, .6)}, data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + }, data) # One of the light profiles - prof_name, prof_x, prof_y, prof_bri = 'relax', 0.5119, 0.4147, 144 + prof_name, prof_h, prof_s, prof_bri = 'relax', 35.932, 69.412, 144 # Test light profiles light.turn_on(self.hass, dev1.entity_id, profile=prof_name) @@ -216,16 +218,16 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_XY_COLOR: (prof_x, prof_y)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: prof_bri, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: 100, - light.ATTR_XY_COLOR: (.5119, .4147)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) # Test bad data light.turn_on(self.hass) @@ -301,15 +303,16 @@ class TestLight(unittest.TestCase): _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_XY_COLOR: (.4, .6), light.ATTR_BRIGHTNESS: 100}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100 + }, data) async def test_intent_set_color(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { - ATTR_SUPPORTED_FEATURES: light.SUPPORT_RGB_COLOR + ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) @@ -364,7 +367,7 @@ async def test_intent_set_color_and_brightness(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { ATTR_SUPPORTED_FEATURES: ( - light.SUPPORT_RGB_COLOR | light.SUPPORT_BRIGHTNESS) + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS) }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 6c56564df69..71fe77ef6be 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -250,12 +250,12 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -303,7 +303,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.675, 0.322], + self.assertEqual((0.652, 0.343), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -458,11 +458,11 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(50, state.attributes.get('brightness')) - self.assertEqual([1, 2, 3], state.attributes.get('rgb_color')) + self.assertEqual((0, 123, 255), state.attributes.get('rgb_color')) self.assertEqual(300, state.attributes.get('color_temp')) self.assertEqual('rainbow', state.attributes.get('effect')) self.assertEqual(75, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.14, 0.131), state.attributes.get('xy_color')) def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name @@ -516,18 +516,18 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '75,75,75', 2, False), + mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.123,0.123', 2, False), + mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.123, 0.123), state.attributes['xy_color']) + self.assertEqual((0.32, 0.336), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -554,12 +554,12 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 0, False), - mock.call('test_light_rgb/rgb/set', '#ff8040', 0, False), + mock.call('test_light_rgb/rgb/set', '#ff803f', 0, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 128, 64), state.attributes['rgb_color']) + self.assertEqual((255, 128, 63), state.attributes['rgb_color']) def test_show_brightness_if_only_command_topic(self): """Test the brightness if only a command topic is present.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" @@ -799,7 +799,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '75,75,75', 0, False), + mock.call('test_light/rgb', '50,50,50', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index ba306a81a34..cfeffc93108 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -180,7 +180,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) @@ -192,8 +192,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255,' - '"x":0.123,"y":0.123},' + '"color":{"r":255,"g":255,"b":255},' '"brightness":255,' '"color_temp":155,' '"effect":"colorloop",' @@ -202,12 +201,12 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -232,7 +231,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -241,7 +240,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.135, 0.135], + self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -503,7 +502,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(255, state.attributes.get('white_value')) @@ -516,7 +515,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Color should not have changed state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # Bad brightness values fire_mqtt_message(self.hass, 'test_light_rgb', diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 5a01aa15fa2..90d68dd10d2 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -151,7 +151,7 @@ class TestLightMQTTTemplate(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) + self.assertEqual((255, 128, 63), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(145, state.attributes.get('color_temp')) self.assertEqual(123, state.attributes.get('white_value')) @@ -185,7 +185,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + self.assertEqual((243, 249, 255), + light_state.attributes.get('rgb_color')) # change the white value fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') @@ -254,7 +255,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( - 'test_light_rgb/set', 'on,50,,,75-75-75', 2, False) + 'test_light_rgb/set', 'on,50,,,50-50-50', 2, False) self.mock_publish.async_publish.reset_mock() # turn on the light with color temp and white val @@ -267,7 +268,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # check the state state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(200, state.attributes['color_temp']) self.assertEqual(139, state.attributes['white_value']) @@ -387,7 +388,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(215, state.attributes.get('color_temp')) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(222, state.attributes.get('white_value')) self.assertEqual('rainbow', state.attributes.get('effect')) @@ -421,7 +422,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # color should not have changed state = self.hass.states.get('light.test') - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # bad white value values fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255') diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index b925b74a7f0..4966b161360 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -4,9 +4,9 @@ from unittest.mock import patch, MagicMock import homeassistant.components.zwave from homeassistant.components.zwave import const from homeassistant.components.light import ( - zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_RGB_COLOR, - SUPPORT_COLOR_TEMP) + zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR, ATTR_WHITE_VALUE, + SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -42,7 +42,7 @@ def test_get_device_detects_colorlight(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) - assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_COLOR def test_get_device_detects_zw098(mock_openzwave): @@ -54,7 +54,23 @@ def test_get_device_detects_zw098(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) assert device.supported_features == ( - SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_COLOR_TEMP) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP) + + +def test_get_device_detects_rgbw_light(mock_openzwave): + """Test get_device returns a color light.""" + node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) + value = MockValue(data=0, node=node) + color = MockValue(data='#0000000000', node=node) + color_channels = MockValue(data=0x1d, node=node) + values = MockLightValues( + primary=value, color=color, color_channels=color_channels) + + device = zwave.get_device(node=node, values=values, node_config={}) + device.value_added() + assert isinstance(device, zwave.ZwaveColorLight) + assert device.supported_features == ( + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def test_dimmer_turn_on(mock_openzwave): @@ -203,7 +219,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert device.brightness == 118 -def test_set_rgb_color(mock_openzwave): +def test_set_hs_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -216,12 +232,12 @@ def test_set_rgb_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_HS_COLOR: (30, 50)}) - assert color.data == '#c896640000' + assert color.data == '#ffbf7f0000' -def test_set_rgbw_color(mock_openzwave): +def test_set_white_value(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -234,9 +250,9 @@ def test_set_rgbw_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_WHITE_VALUE: 200}) - assert color.data == '#c86400c800' + assert color.data == '#ffffffc800' def test_zw098_set_color_temp(mock_openzwave): @@ -273,7 +289,7 @@ def test_rgb_not_supported(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_value(mock_openzwave): @@ -283,7 +299,7 @@ def test_no_color_value(mock_openzwave): values = MockLightValues(primary=value) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_channels_value(mock_openzwave): @@ -294,7 +310,7 @@ def test_no_color_channels_value(mock_openzwave): values = MockLightValues(primary=value, color=color) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_rgb_value_changed(mock_openzwave): @@ -308,12 +324,12 @@ def test_rgb_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) - color.data = '#c896640000' + color.data = '#ffbf800000' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (29.764, 49.804) def test_rgbww_value_changed(mock_openzwave): @@ -327,12 +343,14 @@ def test_rgbww_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_rgbcw_value_changed(mock_openzwave): @@ -346,12 +364,14 @@ def test_rgbcw_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_ct_value_changed(mock_openzwave): diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 12007d2b8ad..1d774248f35 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -4,11 +4,10 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import lock -from tests.common import get_test_home_assistant - - +from tests.common import get_test_home_assistant, mock_service FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' +OPENABLE_LOCK = 'lock.openable_lock' class TestLockDemo(unittest.TestCase): @@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) + + def test_opening(self): + """Test the opening of a lock.""" + calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) + lock.open_lock(self.hass, OPENABLE_LOCK) + self.hass.block_till_done() + self.assertEqual(1, len(calls)) diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index c9267fa8e8e..3377fcefcf5 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -9,7 +9,7 @@ import homeassistant.components.mailbox as mailbox @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" config = { mailbox.DOMAIN: { @@ -18,7 +18,7 @@ def mock_http_client(hass, test_client): } hass.loop.run_until_complete( async_setup_component(hass, mailbox.DOMAIN, config)) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 6acbf5c2db3..11e324e9132 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -5,7 +5,7 @@ import asyncio import homeassistant.components.media_player as mp from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_ON, STATE_OFF, STATE_IDLE) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_home_assistant diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 2075b4cf6e6..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -5,12 +5,17 @@ from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID +import attr import pytest from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ + async_dispatcher_send from homeassistant.components.media_player import cast +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) @@ -26,57 +31,74 @@ def cast_mock(): FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2') -def get_fake_chromecast(host='192.168.178.42', port=8009, - uuid: Optional[UUID] = FakeUUID): +def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" - return MagicMock(host=host, port=port, uuid=uuid) + mock = MagicMock(host=info.host, port=info.port, uuid=info.uuid) + mock.media_controller.status = None + return mock -@asyncio.coroutine -def async_setup_cast(hass, config=None, discovery_info=None): +def get_fake_chromecast_info(host='192.168.178.42', port=8009, + uuid: Optional[UUID] = FakeUUID): + """Generate a Fake ChromecastInfo with the specified arguments.""" + return ChromecastInfo(host=host, port=port, uuid=uuid, + friendly_name="Speaker") + + +async def async_setup_cast(hass, config=None, discovery_info=None): """Helper to setup the cast platform.""" if config is None: config = {} add_devices = Mock() - yield from cast.async_setup_platform(hass, config, add_devices, - discovery_info=discovery_info) - yield from hass.async_block_till_done() + await cast.async_setup_platform(hass, config, add_devices, + discovery_info=discovery_info) + await hass.async_block_till_done() return add_devices -@asyncio.coroutine -def async_setup_cast_internal_discovery(hass, config=None, - discovery_info=None, - no_from_host_patch=False): +async def async_setup_cast_internal_discovery(hass, config=None, + discovery_info=None): """Setup the cast platform and the discovery.""" listener = MagicMock(services={}) with patch('pychromecast.start_discovery', return_value=(listener, None)) as start_discovery: - add_devices = yield from async_setup_cast(hass, config, discovery_info) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() + add_devices = await async_setup_cast(hass, config, discovery_info) + await hass.async_block_till_done() + await hass.async_block_till_done() assert start_discovery.call_count == 1 discovery_callback = start_discovery.call_args[0][0] - def discover_chromecast(service_name, chromecast): + def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = ( - chromecast.host, chromecast.port, chromecast.uuid, None, None) - if no_from_host_patch: - discovery_callback(service_name) - else: - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - discovery_callback(service_name) + listener.services[service_name] = attr.astuple(info) + discovery_callback(service_name) return discover_chromecast, add_devices +async def async_setup_media_player_cast(hass: HomeAssistantType, + info: ChromecastInfo): + """Setup the cast platform with async_setup_component.""" + chromecast = get_fake_chromecast(info) + + cast.CastStatusListener = MagicMock() + + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast) as get_chromecast: + await async_setup_component(hass, 'media_player', { + 'media_player': {'platform': 'cast', 'host': info.host}}) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + assert cast.CastStatusListener.call_count == 1 + entity = cast.CastStatusListener.call_args[0][0] + return chromecast, entity + + @asyncio.coroutine def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" @@ -95,11 +117,13 @@ def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: - yield from async_setup_cast(hass) + # start_discovery should be called with empty config + yield from async_setup_cast(hass, {}) assert start_discovery.call_count == 1 with patch('pychromecast.stop_discovery') as stop_discovery: + # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from hass.async_block_till_done() @@ -107,145 +131,223 @@ def test_stop_discovery_called_on_stop(hass): with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: + # start_discovery should be called again on re-startup yield from async_setup_cast(hass) assert start_discovery.call_count == 1 -@asyncio.coroutine -def test_internal_discovery_callback_only_generates_once(hass): - """Test _get_chromecast_from_host only called once per device.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() +async def test_internal_discovery_callback_only_generates_once(hass): + """Test discovery only called once per device.""" + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info() - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast) as gen_chromecast: - discover_cast('the-service', chromecast) - mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) - gen_chromecast.assert_called_once_with(mdns, blocking=True) + signal = MagicMock() + async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - gen_chromecast.reset_mock() - assert gen_chromecast.call_count == 0 - - -@asyncio.coroutine -def test_internal_discovery_callback_calls_dispatcher(hass): - """Test internal discovery calls dispatcher.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery(hass) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() - - signal.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_internal_discovery_callback_with_connection_error(hass): - """Test internal discovery not calling dispatcher on ConnectionError.""" - import pychromecast # imports mock pychromecast - - pychromecast.ChromecastConnectionError = IOError - - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - side_effect=pychromecast.ChromecastConnectionError): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() + with patch('pychromecast.dial.get_device_status', return_value=None): + # discovering a cast device should call the dispatcher + discover_cast('the-service', info) + await hass.async_block_till_done() + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(info) + signal.reset_mock() + # discovering it a second time shouldn't + discover_cast('the-service', info) + await hass.async_block_till_done() assert signal.call_count == 0 -def test_create_cast_device_without_uuid(hass): - """Test create a cast device without a UUID.""" - chromecast = get_fake_chromecast(uuid=None) - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - - -def test_create_cast_device_with_uuid(hass): - """Test create cast devices with UUID.""" - added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - chromecast = get_fake_chromecast() - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - assert chromecast.uuid in added_casts - - with patch.object(cast_device, 'async_set_chromecast') as mock_set: - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 0 - - chromecast = get_fake_chromecast(host='192.168.178.1') - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 1 - mock_set.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_normal_chromecast_not_starting_discovery(hass): - """Test cast platform not starting discovery when not required.""" +async def test_internal_discovery_callback_fill_out(hass): + """Test internal discovery automatically filling out information.""" import pychromecast # imports mock pychromecast pychromecast.ChromecastConnectionError = IOError - chromecast = get_fake_chromecast() + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(uuid=None) + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) - with patch('pychromecast.Chromecast', return_value=chromecast): - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + signal = MagicMock() + + async_dispatcher_connect(hass, 'cast_discovered', signal) + discover_cast('the-service', info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(full_info) + + +async def test_create_cast_device_without_uuid(hass): + """Test create a cast device with no UUId should still create an entity.""" + info = get_fake_chromecast_info(uuid=None) + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + + +async def test_create_cast_device_with_uuid(hass): + """Test create cast devices with UUID creates entities.""" + added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + info = get_fake_chromecast_info() + + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + assert info.uuid in added_casts + + # Sending second time should not create new entity + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is None + + +async def test_normal_chromecast_not_starting_discovery(hass): + """Test cast platform not starting discovery when not required.""" + # pylint: disable=no-member + with patch('homeassistant.components.media_player.cast.' + '_setup_internal_discovery') as setup_discovery: + # normal (non-group) chromecast shouldn't start discovery. + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 # Same entity twice - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 8009}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + # group should start discovery. + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 42}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 1 - with patch('pychromecast.Chromecast', - side_effect=pychromecast.ChromecastConnectionError): + +async def test_normal_raises_platform_not_ready(hass): + """Test cast platform raises PlatformNotReady if HTTP dial fails.""" + with patch('pychromecast.dial.get_device_status', return_value=None): with pytest.raises(PlatformNotReady): - yield from async_setup_cast(hass, {'host': 'host3'}) + await async_setup_cast(hass, {'host': 'host1'}) -@asyncio.coroutine -def test_replay_past_chromecasts(hass): +async def test_replay_past_chromecasts(hass): """Test cast platform re-playing past chromecasts when adding new one.""" - cast_group1 = get_fake_chromecast(host='host1', port=42) - cast_group2 = get_fake_chromecast(host='host2', port=42, uuid=UUID( + cast_group1 = get_fake_chromecast_info(host='host1', port=42) + cast_group2 = get_fake_chromecast_info(host='host2', port=42, uuid=UUID( '9462202c-e747-4af5-a66b-7dce0e1ebc09')) - discover_cast, add_dev1 = yield from async_setup_cast_internal_discovery( + discover_cast, add_dev1 = await async_setup_cast_internal_discovery( hass, discovery_info={'host': 'host1', 'port': 42}) discover_cast('service2', cast_group2) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev1.call_count == 0 discover_cast('service1', cast_group1) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() # having jobs that add jobs + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 - add_dev2 = yield from async_setup_cast( + add_dev2 = await async_setup_cast( hass, discovery_info={'host': 'host2', 'port': 42}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev2.call_count == 1 + + +async def test_entity_media_states(hass: HomeAssistantType): + """Test various entity media states.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + state = hass.states.get('media_player.speaker') + assert state is not None + assert state.name == 'Speaker' + assert state.state == 'unknown' + assert entity.unique_id == full_info.uuid + + media_status = MagicMock(images=None) + media_status.player_is_playing = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'playing' + + entity.new_media_status(media_status) + media_status.player_is_playing = False + media_status.player_is_paused = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'paused' + + entity.new_media_status(media_status) + media_status.player_is_paused = False + media_status.player_is_idle = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'idle' + + media_status.player_is_idle = False + chromecast.is_idle = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'off' + + chromecast.is_idle = False + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'unknown' + + +async def test_switched_host(hass: HomeAssistantType): + """Test cast device listens for changed hosts and disconnects old cast.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, _ = await async_setup_media_player_cast(hass, full_info) + + chromecast2 = get_fake_chromecast(info) + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast2) as get_chromecast: + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, full_info) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(full_info, friendly_name='Speaker 2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(changed, host='host2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + chromecast.disconnect.assert_called_once_with(blocking=False) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + chromecast.disconnect.assert_called_once_with(blocking=False) diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 3470c79ad64..7d0d675f66f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -9,8 +9,7 @@ from soco import alarms from homeassistant.setup import setup_component from homeassistant.components.media_player import sonos, DOMAIN -from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR, \ - CONF_ADVERTISE_ADDR +from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR from homeassistant.const import CONF_HOSTS, CONF_PLATFORM from tests.common import get_test_home_assistant @@ -162,7 +161,7 @@ class TestSonosMediaPlayer(unittest.TestCase): 'host': '192.0.2.1' }) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -185,27 +184,6 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) self.assertEqual(discover_mock.call_count, 1) - @mock.patch('soco.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch('soco.discover') - def test_ensure_setup_config_advertise_addr(self, discover_mock, - *args): - """Test an advertise address config'd by the HASS config file.""" - discover_mock.return_value = {SoCoMock('192.0.2.1')} - - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_ADVERTISE_ADDR: '192.0.1.1', - } - } - - assert setup_component(self.hass, DOMAIN, config) - - self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) - self.assertEqual(discover_mock.call_count, 1) - self.assertEqual(soco.config.EVENT_ADVERTISE_IP, '192.0.1.1') - @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_string_single(self, *args): @@ -263,7 +241,7 @@ class TestSonosMediaPlayer(unittest.TestCase): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass)) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -275,7 +253,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(30) @@ -289,7 +267,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(None) @@ -298,12 +276,12 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('soco.alarms.Alarm') @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_update_alarm(self, soco_mock, alarm_mock, *args): + def test_set_alarm(self, soco_mock, alarm_mock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass alarm1 = alarms.Alarm(soco_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, @@ -315,9 +293,9 @@ class TestSonosMediaPlayer(unittest.TestCase): 'include_linked_zones': True, 'volume': 0.30, } - device.update_alarm(alarm_id=2) + device.set_alarm(alarm_id=2) alarm1.save.assert_not_called() - device.update_alarm(alarm_id=1, **attrs) + device.set_alarm(alarm_id=1, **attrs) self.assertEqual(alarm1.enabled, attrs['enabled']) self.assertEqual(alarm1.start_time, attrs['time']) self.assertEqual(alarm1.include_linked_zones, @@ -333,7 +311,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass snapshotMock.return_value = True @@ -351,7 +329,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass restoreMock.return_value = True diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index b7f3165f11c..c9a1cdc79d8 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -11,7 +11,7 @@ import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.media_player.universal as universal -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1dd89a92f04..b25479bb75a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -59,7 +59,7 @@ class TestMQTTComponent(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_stops_on_home_assistant_start(self): + def aiohttp_client_stops_on_home_assistant_start(self): """Test if client stops on HA stop.""" self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) self.hass.block_till_done() @@ -156,7 +156,7 @@ class TestMQTTCallbacks(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_starts_on_home_assistant_mqtt_setup(self): + def aiohttp_client_starts_on_home_assistant_mqtt_setup(self): """Test if client is connected after mqtt init on bootstrap.""" self.assertEqual(self.hass.data['mqtt']._mqttc.connect.call_count, 1) diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index ed988b0f9b5..c96a49d7cb3 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from homeassistant.setup import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import group, demo -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 9ec71020ef1..318f3c7512c 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -49,7 +49,7 @@ REGISTER_URL = '/api/notify.html5' PUBLISH_URL = '/api/notify.html5/callback' -async def mock_client(hass, test_client, registrations=None): +async def mock_client(hass, aiohttp_client, registrations=None): """Create a test client for HTML5 views.""" if registrations is None: registrations = {} @@ -62,7 +62,7 @@ async def mock_client(hass, test_client, registrations=None): } }) - return await test_client(hass.http.app) + return await aiohttp_client(hass.http.app) class TestHtml5Notify(object): @@ -151,9 +151,9 @@ class TestHtml5Notify(object): assert mock_wp.mock_calls[4][2]['gcm_key'] is None -async def test_registering_new_device_view(hass, test_client): +async def test_registering_new_device_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -165,9 +165,9 @@ async def test_registering_new_device_view(hass, test_client): } -async def test_registering_new_device_expiration_view(hass, test_client): +async def test_registering_new_device_expiration_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) @@ -178,10 +178,10 @@ async def test_registering_new_device_expiration_view(hass, test_client): } -async def test_registering_new_device_fails_view(hass, test_client): +async def test_registering_new_device_fails_view(hass, aiohttp_client): """Test subs. are not altered when registering a new device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -191,10 +191,10 @@ async def test_registering_new_device_fails_view(hass, test_client): assert registrations == {} -async def test_registering_existing_device_view(hass, test_client): +async def test_registering_existing_device_view(hass, aiohttp_client): """Test subscription is updated when registering existing device.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -209,10 +209,10 @@ async def test_registering_existing_device_view(hass, test_client): } -async def test_registering_existing_device_fails_view(hass, test_client): +async def test_registering_existing_device_fails_view(hass, aiohttp_client): """Test sub. is not updated when registering existing device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -225,9 +225,9 @@ async def test_registering_existing_device_fails_view(hass, test_client): } -async def test_registering_new_device_validation(hass, test_client): +async def test_registering_new_device_validation(hass, aiohttp_client): """Test various errors when registering a new device.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(REGISTER_URL, data=json.dumps({ 'browser': 'invalid browser', @@ -249,13 +249,13 @@ async def test_registering_new_device_validation(hass, test_client): assert resp.status == 400 -async def test_unregistering_device_view(hass, test_client): +async def test_unregistering_device_view(hass, aiohttp_client): """Test that the HTML unregister view works.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -269,11 +269,11 @@ async def test_unregistering_device_view(hass, test_client): } -async def test_unregister_device_view_handle_unknown_subscription(hass, - test_client): +async def test_unregister_device_view_handle_unknown_subscription( + hass, aiohttp_client): """Test that the HTML unregister view handles unknown subscriptions.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -285,13 +285,14 @@ async def test_unregister_device_view_handle_unknown_subscription(hass, assert len(mock_save.mock_calls) == 0 -async def test_unregistering_device_view_handles_save_error(hass, test_client): +async def test_unregistering_device_view_handles_save_error( + hass, aiohttp_client): """Test that the HTML unregister view handles save errors.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -306,9 +307,9 @@ async def test_unregistering_device_view_handles_save_error(hass, test_client): } -async def test_callback_view_no_jwt(hass, test_client): +async def test_callback_view_no_jwt(hass, aiohttp_client): """Test that the notification callback view works without JWT.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72' @@ -317,12 +318,12 @@ async def test_callback_view_no_jwt(hass, test_client): assert resp.status == 401, resp.response -async def test_callback_view_with_jwt(hass, test_client): +async def test_callback_view_with_jwt(hass, aiohttp_client): """Test that the notification callback view works with JWT.""" registrations = { 'device': SUBSCRIPTION_1 } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('pywebpush.WebPusher') as mock_wp: await hass.services.async_call('notify', 'notify', { diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 7ee04b0df4c..9300ecef432 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -2,16 +2,69 @@ import re import unittest from unittest.mock import MagicMock, patch +from datetime import timedelta -import forecastio from requests.exceptions import HTTPError import requests_mock -from datetime import timedelta + +import forecastio from homeassistant.components.sensor import darksky from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import (load_fixture, get_test_home_assistant, + MockDependency) + +VALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['sumary', 'iocn', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +VALID_CONFIG_LANG_DE = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'units': 'us', + 'language': 'de', + 'monitored_conditions': ['summary', 'icon', 'temperature_max', + 'minutely_summary', 'hourly_summary', + 'daily_summary', 'humidity', ], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_LANG = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'language': 'yz', + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + + +def load_forecastMock(key, lat, lon, + units, lang): # pylint: disable=invalid-name + """Mock darksky forecast loading.""" + return '' class TestDarkSkySetup(unittest.TestCase): @@ -30,12 +83,6 @@ class TestDarkSkySetup(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.key = 'foo' - self.config = { - 'api_key': 'foo', - 'forecast': [1, 2], - 'monitored_conditions': ['summary', 'icon', 'temperature_max'], - 'update_interval': timedelta(seconds=120), - } self.lat = self.hass.config.latitude = 37.8267 self.lon = self.hass.config.longitude = -122.423 self.entities = [] @@ -44,10 +91,41 @@ class TestDarkSkySetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_setup_with_config(self): + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" - self.assertTrue( - setup_component(self.hass, 'sensor', {'darksky': self.config})) + setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_config(self, mock_forecastio): + """Test the platform setup with invalid configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', VALID_CONFIG_LANG_DE) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_LANG) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None @patch('forecastio.api.get_forecast') def test_setup_bad_api_key(self, mock_get_forecast): @@ -60,7 +138,8 @@ class TestDarkSkySetup(unittest.TestCase): msg = '400 Client Error: Bad Request for url: {}'.format(url) mock_get_forecast.side_effect = HTTPError(msg,) - response = darksky.setup_platform(self.hass, self.config, MagicMock()) + response = darksky.setup_platform(self.hass, VALID_CONFIG_MINIMAL, + MagicMock()) self.assertFalse(response) @requests_mock.Mocker() @@ -69,9 +148,16 @@ class TestDarkSkySetup(unittest.TestCase): """Test for successfully setting up the forecast.io platform.""" uri = (r'https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/' r'(-?\d+\.?\d*),(-?\d+\.?\d*)') - mock_req.get(re.compile(uri), - text=load_fixture('darksky.json')) - darksky.setup_platform(self.hass, self.config, self.add_entities) + mock_req.get(re.compile(uri), text=load_fixture('darksky.json')) + + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) - self.assertEqual(len(self.entities), 7) + self.assertEqual(len(self.hass.states.entity_ids()), 7) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + self.assertEqual(state.state, 'Clear') + self.assertEqual(state.attributes.get('friendly_name'), + 'Dark Sky Summary') diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index dd1112d65f8..0d4082731ab 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -1,8 +1,11 @@ """The test for the data filter sensor platform.""" +from datetime import timedelta import unittest +from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) +import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -90,3 +93,16 @@ class TestFilterSensor(unittest.TestCase): if not filt.skip_processing: filtered.append(new_state) self.assertEqual([20, 21], filtered) + + def test_time_sma(self): + """Test if time_sma filter works.""" + filt = TimeSMAFilter(window_size=timedelta(minutes=2), + precision=2, + entity=None, + type='last') + past = dt_util.utcnow() - timedelta(minutes=5) + for state in self.values: + with patch('homeassistant.util.dt.utcnow', return_value=past): + filtered = filt.filter_state(state) + past += timedelta(minutes=1) + self.assertEqual(21.5, filtered) diff --git a/tests/components/sensor/test_foobot.py b/tests/components/sensor/test_foobot.py new file mode 100644 index 00000000000..322f2b3f2a8 --- /dev/null +++ b/tests/components/sensor/test_foobot.py @@ -0,0 +1,81 @@ +"""The tests for the Foobot sensor platform.""" + +import re +import asyncio +from unittest.mock import MagicMock +import pytest + + +import homeassistant.components.sensor as sensor +from homeassistant.components.sensor import foobot +from homeassistant.const import (TEMP_CELSIUS) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.setup import async_setup_component +from tests.common import load_fixture + +VALID_CONFIG = { + 'platform': 'foobot', + 'token': 'adfdsfasd', + 'username': 'example@example.com', +} + + +async def test_default_setup(hass, aioclient_mock): + """Test the default setup.""" + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + text=load_fixture('foobot_devices.json')) + aioclient_mock.get(re.compile('api.foobot.io/v2/device/.*'), + text=load_fixture('foobot_data.json')) + assert await async_setup_component(hass, sensor.DOMAIN, + {'sensor': VALID_CONFIG}) + + metrics = {'co2': ['1232.0', 'ppm'], + 'temperature': ['21.1', TEMP_CELSIUS], + 'humidity': ['49.5', '%'], + 'pm25': ['144.8', 'µg/m3'], + 'voc': ['340.7', 'ppb'], + 'index': ['138.9', '%']} + + for name, value in metrics.items(): + state = hass.states.get('sensor.foobot_happybot_%s' % name) + assert state.state == value[0] + assert state.attributes.get('unit_of_measurement') == value[1] + + +async def test_setup_timeout_error(hass, aioclient_mock): + """Expected failures caused by a timeout in API response.""" + fake_async_add_devices = MagicMock() + + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + exc=asyncio.TimeoutError()) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, {'sensor': VALID_CONFIG}, + fake_async_add_devices) + + +async def test_setup_permanent_error(hass, aioclient_mock): + """Expected failures caused by permanent errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [400, 401, 403] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + result = await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) + assert result is None + + +async def test_setup_temporary_error(hass, aioclient_mock): + """Expected failures caused by temporary errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [429, 500] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) diff --git a/tests/components/sensor/test_mhz19.py b/tests/components/sensor/test_mhz19.py index 6948a952c31..6d071489691 100644 --- a/tests/components/sensor/test_mhz19.py +++ b/tests/components/sensor/test_mhz19.py @@ -52,7 +52,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', side_effect=OSError('test error')) - def test_client_update_oserror(self, mock_function): + def aiohttp_client_update_oserror(self, mock_function): """Test MHZClient when library throws OSError.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -61,7 +61,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(5001, 24)) - def test_client_update_ppm_overflow(self, mock_function): + def aiohttp_client_update_ppm_overflow(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -70,7 +70,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(1000, 24)) - def test_client_update_good_read(self, mock_function): + def aiohttp_client_update_good_read(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 5e258bc9245..b05fc90bfe4 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -131,6 +131,33 @@ class TestTemplateSensor: state = self.hass.states.get('sensor.test_template_sensor') assert state.attributes['friendly_name'] == 'It Works.' + def test_friendly_name_template_with_unknown_state(self): + """Test friendly_name template with an unknown value_template.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "{{ states.fourohfour.state }}", + 'friendly_name_template': + "It {{ states.sensor.test_state.state }}." + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_template_sensor') + assert state.attributes['friendly_name'] == 'It .' + + self.hass.states.set('sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test_template_sensor') + assert state.attributes['friendly_name'] == 'It Works.' + def test_template_syntax_error(self): """Test templating syntax error.""" with assert_setup_component(0): diff --git a/tests/components/sensor/test_uptime.py b/tests/components/sensor/test_uptime.py index 541ea7ca771..a919e7d20db 100644 --- a/tests/components/sensor/test_uptime.py +++ b/tests/components/sensor/test_uptime.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import patch from datetime import timedelta -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.setup import setup_component from homeassistant.components.sensor.uptime import UptimeSensor from tests.common import get_test_home_assistant diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 27047ba0ad0..65526e2d938 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -143,3 +143,23 @@ def test_invalid_data(hass, aioclient_mock): for condition in VALID_CONFIG['monitored_conditions']: state = hass.states.get('sensor.pws_' + condition) assert state.state == STATE_UNKNOWN + + +async def test_entity_id_with_multiple_stations(hass, aioclient_mock): + """Test not generating duplicate entity ids with multiple stations.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'} + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + state = hass.states.get('sensor.pws_weather') + assert state is not None + assert state.state == 'Clear' + + state = hass.states.get('sensor.hi_weather') + assert state is not None + assert state.state == 'Clear' diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index 064d0b1825b..e3f11ec19a0 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -5,7 +5,7 @@ import aiohttp import homeassistant.components.switch.rest as rest from homeassistant.setup import setup_component -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant, assert_setup_component diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 69b9bfa69de..6d5bec046f1 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -11,10 +11,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_api_client(hass, test_client): +def mock_api_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'api', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 466dc57017a..bde00e10928 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -93,7 +93,7 @@ def test_register_before_setup(hass): @asyncio.coroutine -def test_http_processing_intent(hass, test_client): +def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): intent_type = 'OrderBeer' @@ -122,7 +122,7 @@ def test_http_processing_intent(hass, test_client): }) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -224,7 +224,7 @@ def test_toggle_intent(hass, sentence): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -232,7 +232,7 @@ def test_http_api(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -249,7 +249,7 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_wrong_data(hass, test_client): +def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -257,7 +257,7 @@ def test_http_api_wrong_data(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 123 diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c4ade7f5c19..c742e215738 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -12,14 +12,14 @@ from homeassistant.components.frontend import ( @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_themes(hass, test_client): +def mock_http_client_with_themes(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -29,11 +29,11 @@ def mock_http_client_with_themes(hass, test_client): } } }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_urls(hass, test_client): +def mock_http_client_with_urls(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -42,7 +42,7 @@ def mock_http_client_with_urls(hass, test_client): CONF_EXTRA_HTML_URL_ES5: ["https://domain.com/my_extra_url_es5.html"] }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 4a759e7e0ac..bea2af396cb 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -4,7 +4,7 @@ from datetime import timedelta import unittest from unittest.mock import patch, sentinel -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder @@ -481,3 +481,15 @@ class TestComponentHistory(unittest.TestCase): set_state(therm, 22, attributes={'current_temperature': 21, 'hidden': True}) return zero, four, states + + +async def test_fetch_period_api(hass, aiohttp_client): + """Test the fetch period view for history.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'history', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py deleted file mode 100644 index fa61cb2b69e..00000000000 --- a/tests/components/test_hue.py +++ /dev/null @@ -1,588 +0,0 @@ -"""Generic Philips Hue component tests.""" -import asyncio -import logging -import unittest -from unittest.mock import call, MagicMock, patch - -import aiohue -import pytest -import voluptuous as vol - -from homeassistant.components import configurator, hue -from homeassistant.const import CONF_FILENAME, CONF_HOST -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import ( - assert_setup_component, get_test_home_assistant, get_test_config_dir, - MockDependency, MockConfigEntry, mock_coro -) - -_LOGGER = logging.getLogger(__name__) - - -class TestSetup(unittest.TestCase): - """Test the Hue component.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_no_domain(self, mock_phue): - """If it's not in the config we won't even try.""" - with assert_setup_component(0): - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - mock_phue.Bridge.assert_not_called() - self.assertEqual({}, self.hass.data[hue.DOMAIN]) - - @MockDependency('phue') - def test_setup_with_host(self, mock_phue): - """Host specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_phue_conf(self, mock_phue): - """No host in the config file, but one is cached in phue.conf.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch( - 'homeassistant.components.hue._find_host_from_config', - return_value='localhost'): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_FILENAME: 'phue.conf'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_multiple_hosts(self, mock_phue): - """Multiple hosts specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}, - {CONF_HOST: '192.168.0.1'}]}})) - - mock_bridge.assert_has_calls([ - call( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)), - call( - '192.168.0.1', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE))]) - mock_load.mock_bridge.assert_not_called() - mock_load.assert_has_calls([ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}), - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.1'}), - ], any_order=True) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(2, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_discovered(self, mock_phue): - """Bridge discovery.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'} - - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - mock_bridge.assert_called_once_with( - '192.168.0.10', - config_file_path=get_test_config_dir('phue-foobar.conf')) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.10'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_configure_and_discovered(self, mock_phue): - """Bridge is in the config file, then we discover it.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'} - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - # First we set up the component from config - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: '192.168.1.10'}]}})) - - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - calls_to_mock_load = [ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.1.10'}), - ] - mock_load.assert_has_calls(calls_to_mock_load) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - # Then we discover the same bridge - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - # No additional calls - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_has_calls(calls_to_mock_load) - - # Still only one - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - -class TestHueBridge(unittest.TestCase): - """Test the HueBridge class.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.data[hue.DOMAIN] = {} - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_bridge_connection_refused(self, mock_phue): - """Test a registration failed with a connection refused exception.""" - mock_bridge = mock_phue.Bridge - mock_bridge.side_effect = ConnectionRefusedError() - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_exception(self, mock_phue): - """Test a registration failed with an exception.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2) - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - self.assertTrue(isinstance(bridge.config_request_id, str)) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_succeeds(self, mock_phue): - """Test a registration success sequence.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, registration is done - None, - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertTrue(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configured', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_fails(self, mock_phue): - """ - Test a registration failure sequence. - - This may happen when we start the registration process, the user - responds to the request but the bridge has become unreachable. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, the bridge has gone away - ConnectionRefusedError(), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # The request should still be pending - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_retry(self, mock_phue): - """ - Test a registration retry sequence. - - This may happen when we start the registration process, the user - responds to the request but we fail to confirm it with the bridge. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, for whatever reason authentication fails - mock_phue.PhueRegistrationException(1, 2), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - self.assertEqual( - 'Failed to register, please try again.', - self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS)) - - @MockDependency('phue') - def test_hue_activate_scene(self, mock_phue): - """Test the hue_activate_scene service.""" - with patch('homeassistant.helpers.discovery.load_platform'): - bridge = hue.HueBridge('localhost', self.hass, - hue.PHUE_CONFIG_FILE, None) - bridge.setup() - - # No args - self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Only one arg - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Both required args - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_called_once_with('group', 'scene') - - -async def test_setup_no_host(hass, requests_mock): - """No host specified in any way.""" - requests_mock.get(hue.API_NUPNP, json=[]) - with MockDependency('phue') as mock_phue: - result = await async_setup_component( - hass, hue.DOMAIN, {hue.DOMAIN: {}}) - assert result - - mock_phue.Bridge.assert_not_called() - - assert hass.data[hue.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow .""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - - flow = hue.HueFlowHandler() - flow.hass = hass - await flow.async_step_init() - - with patch('aiohue.Bridge') as mock_bridge: - def mock_constructor(host, websession): - mock_bridge.host = host - return mock_bridge - - mock_bridge.side_effect = mock_constructor - mock_bridge.username = 'username-abc' - mock_bridge.config.name = 'Mock Bridge' - mock_bridge.config.bridgeid = 'bridge-id-1234' - mock_bridge.create_user.return_value = mock_coro() - mock_bridge.initialize.return_value = mock_coro() - - result = await flow.async_step_link(user_input={}) - - assert mock_bridge.host == '1.2.3.4' - assert len(mock_bridge.create_user.mock_calls) == 1 - assert len(mock_bridge.initialize.mock_calls) == 1 - - assert result['type'] == 'create_entry' - assert result['title'] == 'Mock Bridge' - assert result['data'] == { - 'host': '1.2.3.4', - 'bridge_id': 'bridge-id-1234', - 'username': 'username-abc' - } - - -async def test_flow_no_discovered_bridges(hass, aioclient_mock): - """Test config flow discovers no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): - """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_one_bridge_discovered(hass, aioclient_mock): - """Test config flow discovers one bridge.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - - -async def test_flow_two_bridges_discovered(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'init' - - with pytest.raises(vol.Invalid): - assert result['data_schema']({'host': '0.0.0.0'}) - - result['data_schema']({'host': '1.2.3.4'}) - result['data_schema']({'host': '5.6.7.8'}) - - -async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert flow.host == '5.6.7.8' - - -async def test_flow_timeout_discovery(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.discovery.discover_nupnp', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_init() - - assert result['type'] == 'abort' - - -async def test_flow_link_timeout(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'Failed to register, please try again.' - } - - -async def test_flow_link_button_not_pressed(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'Failed to register, please try again.' - } - - -async def test_flow_link_unknown_host(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.RequestError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'Failed to register, please try again.' - } diff --git a/tests/components/test_init.py b/tests/components/test_init.py index eca4763b4b3..991982af9b2 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -14,7 +14,7 @@ import homeassistant.components as comps import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, mock_service, patch_yaml_files, mock_coro, diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index bd10416c7a2..6c71a263afa 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -10,8 +10,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util -from homeassistant.components import logbook -from homeassistant.setup import setup_component +from homeassistant.components import logbook, recorder +from homeassistant.setup import setup_component, async_setup_component from tests.common import ( init_recorder_component, get_test_home_assistant) @@ -555,3 +555,15 @@ class TestComponentLogbook(unittest.TestCase): 'old_state': state, 'new_state': state, }, time_fired=event_time_fired) + + +async def test_logbook_view(hass, aiohttp_client): + """Test the logbook view.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/logbook/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 052292b015d..6cc0e4fcada 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -7,14 +7,14 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture -def prometheus_client(loop, hass, test_client): - """Initialize a test_client with Prometheus component.""" +def prometheus_client(loop, hass, aiohttp_client): + """Initialize a aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, {}, )) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_ring.py b/tests/components/test_ring.py index 819f447f2f5..3837ec13061 100644 --- a/tests/components/test_ring.py +++ b/tests/components/test_ring.py @@ -1,4 +1,5 @@ """The tests for the Ring component.""" +from copy import deepcopy import os import unittest import requests_mock @@ -51,7 +52,7 @@ class TestRing(unittest.TestCase): """Test the setup when no login is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['username'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) @@ -60,6 +61,6 @@ class TestRing(unittest.TestCase): """Test the setup when no password is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['password'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py index 8b16b5519e9..36f68e57c9f 100644 --- a/tests/components/test_rss_feed_template.py +++ b/tests/components/test_rss_feed_template.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_http_client(loop, hass, test_client): +def mock_http_client(loop, hass, aiohttp_client): """Setup test fixture.""" config = { 'rss_feed_template': { @@ -21,7 +21,7 @@ def mock_http_client(loop, hass, test_client): loop.run_until_complete(async_setup_component(hass, 'rss_feed_template', config)) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py index 4203f7587ae..3131ae092a3 100644 --- a/tests/components/test_shopping_list.py +++ b/tests/components/test_shopping_list.py @@ -54,7 +54,7 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_api_get_all(hass, test_client): +def test_api_get_all(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -65,7 +65,7 @@ def test_api_get_all(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/shopping_list') assert resp.status == 200 @@ -78,7 +78,7 @@ def test_api_get_all(hass, test_client): @asyncio.coroutine -def test_api_update(hass, test_client): +def test_api_update(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -92,7 +92,7 @@ def test_api_update(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/item/{}'.format(beer_id), json={ 'name': 'soda' @@ -133,7 +133,7 @@ def test_api_update(hass, test_client): @asyncio.coroutine -def test_api_update_fails(hass, test_client): +def test_api_update_fails(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -141,7 +141,7 @@ def test_api_update_fails(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/non_existing', json={ 'name': 'soda' @@ -159,7 +159,7 @@ def test_api_update_fails(hass, test_client): @asyncio.coroutine -def test_api_clear_completed(hass, test_client): +def test_api_clear_completed(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -173,7 +173,7 @@ def test_api_clear_completed(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) # Mark beer as completed resp = yield from client.post( @@ -196,11 +196,11 @@ def test_api_clear_completed(hass, test_client): @asyncio.coroutine -def test_api_create(hass, test_client): +def test_api_create(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 'soda' }) @@ -217,11 +217,11 @@ def test_api_create(hass, test_client): @asyncio.coroutine -def test_api_create_fail(hass, test_client): +def test_api_create_fail(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 1234 }) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index d119c60dba2..c440ef9c30c 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -14,16 +14,16 @@ _LOGGER = logging.getLogger('test_logger') @pytest.fixture(autouse=True) @asyncio.coroutine -def setup_test_case(hass, test_client): +def setup_test_case(hass, aiohttp_client): """Setup system_log component before test case.""" config = {'system_log': {'max_entries': 2}} yield from async_setup_component(hass, system_log.DOMAIN, config) @asyncio.coroutine -def get_error_log(hass, test_client, expected_count): +def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/error/all') assert resp.status == 200 @@ -53,41 +53,41 @@ def get_frame(name): @asyncio.coroutine -def test_normal_logs(hass, test_client): +def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine -def test_exception(hass, test_client): +def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') @asyncio.coroutine -def test_warning(hass, test_client): +def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') @asyncio.coroutine -def test_error(hass, test_client): +def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') @asyncio.coroutine -def test_error_posted_as_event(hass, test_client): +def test_error_posted_as_event(hass, aiohttp_client): """Test that error are posted as events.""" events = [] @@ -106,26 +106,26 @@ def test_error_posted_as_event(hass, test_client): @asyncio.coroutine -def test_critical(hass, test_client): +def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') @asyncio.coroutine -def test_remove_older_logs(hass, test_client): +def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, test_client, 2) + log = yield from get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') @asyncio.coroutine -def test_clear_logs(hass, test_client): +def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" _LOGGER.error('error message') @@ -135,7 +135,7 @@ def test_clear_logs(hass, test_client): yield from hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine @@ -182,12 +182,12 @@ def test_write_choose_level(hass): @asyncio.coroutine -def test_unknown_path(hass, test_client): +def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -207,30 +207,30 @@ def log_error_from_test_path(path): @asyncio.coroutine -def test_homeassistant_path(hass, test_client): +def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' @asyncio.coroutine -def test_config_path(hass, test_client): +def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' @asyncio.coroutine -def test_netdisco_path(hass, test_client): +def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index d0c129e512e..4deccf65209 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -16,12 +16,12 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, test_client): +def websocket_client(loop, hass, aiohttp_client): """Websocket client fixture connected to websocket server.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api')) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) assert auth_ok['type'] == wapi.TYPE_AUTH_OK @@ -33,7 +33,7 @@ def websocket_client(loop, hass, test_client): @pytest.fixture -def no_auth_websocket_client(hass, loop, test_client): +def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api', { @@ -42,7 +42,7 @@ def no_auth_websocket_client(hass, loop, test_client): } })) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 527101f6c61..bb073459b48 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -17,7 +17,7 @@ from homeassistant.setup import setup_component import pytest from tests.common import ( - get_test_home_assistant, async_fire_time_changed) + get_test_home_assistant, async_fire_time_changed, mock_coro) from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues @@ -213,9 +213,7 @@ def test_node_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -237,7 +235,6 @@ def test_node_ignored(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, 'device_config': { 'zwave.mock_node': { 'ignored': True, @@ -262,9 +259,7 @@ def test_value_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -289,9 +284,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -336,9 +329,7 @@ def test_power_schemes(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -380,9 +371,7 @@ def test_network_ready(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -409,9 +398,7 @@ def test_network_complete(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -441,9 +428,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass = get_test_home_assistant() self.hass.start() - setup_component(self.hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() self.node = MockNode() @@ -472,9 +457,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): command_class='mock_bad_class', node=self.node) self.entity_id = 'mock_component.mock_node_mock_value' - self.zwave_config = {'zwave': { - 'new_entity_ids': True, - }} + self.zwave_config = {'zwave': {}} self.device_config = {self.entity_id: {}} def tearDown(self): # pylint: disable=invalid-name @@ -485,6 +468,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_discovery(self, discovery, get_platform): """Test the creation of a new entity.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -517,8 +501,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -549,6 +532,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_existing_values(self, discovery, get_platform): """Test the loading of already discovered values.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -580,8 +564,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -616,6 +599,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): """Test ignore workaround.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -646,8 +630,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass.block_till_done() assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[1] == 'binary_sensor' @@ -781,9 +764,7 @@ class TestZWaveServices(unittest.TestCase): self.hass.start() # Initialize zwave - setup_component(self.hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() self.zwave_network = self.hass.data[DATA_NETWORK] self.zwave_network.state = MockNetwork.STATE_READY @@ -1014,8 +995,21 @@ class TestZWaveServices(unittest.TestCase): type=const.TYPE_LIST, data_items=['item1', 'item2', 'item3'], ) + value_list_int = MockValue( + index=15, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_LIST, + data_items=['1', '2', '3'], + ) + value_button = MockValue( + index=14, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_BUTTON, + ) node = MockNode(node_id=14) - node.get_values.return_value = {12: value, 13: value_list} + node.get_values.return_value = {12: value, 13: value_list, + 14: value_button, + 15: value_list_int} self.zwave_network.nodes = {14: node} self.hass.services.call('zwave', 'set_config_parameter', { @@ -1027,6 +1021,15 @@ class TestZWaveServices(unittest.TestCase): assert value_list.data == 'item3' + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 15, + const.ATTR_CONFIG_VALUE: 3, + }) + self.hass.block_till_done() + + assert value_list_int.data == '3' + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 12, @@ -1036,6 +1039,16 @@ class TestZWaveServices(unittest.TestCase): assert value.data == 7 + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 14, + const.ATTR_CONFIG_VALUE: True, + }) + self.hass.block_till_done() + + assert self.zwave_network.manager.pressButton.called + assert self.zwave_network.manager.releaseButton.called + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 19, diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index e4afca31740..299821d3685 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -43,7 +43,7 @@ def test_node_event_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -86,7 +86,7 @@ def test_scene_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -129,7 +129,7 @@ def test_central_scene_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -185,7 +185,7 @@ class TestZWaveNodeEntity(unittest.TestCase): self.node.manufacturer_name = 'Test Manufacturer' self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, - self.zwave_network, True) + self.zwave_network) def test_network_node_changed_from_value(self): """Test for network_node_changed.""" @@ -226,8 +226,6 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', - 'old_entity_id': 'zwave.mock_node_567', - 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product'}, self.entity.device_state_attributes) @@ -286,8 +284,6 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', - 'old_entity_id': 'zwave.mock_node_567', - 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product', 'query_stage': 'Dynamic', 'is_awake': True, diff --git a/tests/fixtures/foobot_data.json b/tests/fixtures/foobot_data.json new file mode 100644 index 00000000000..93518614c42 --- /dev/null +++ b/tests/fixtures/foobot_data.json @@ -0,0 +1,34 @@ +{ + "uuid": "32463564765421243", + "start": 1518134963, + "end": 1518134963, + "sensors": [ + "time", + "pm", + "tmp", + "hum", + "co2", + "voc", + "allpollu" + ], + "units": [ + "s", + "ugm3", + "C", + "pc", + "ppm", + "ppb", + "%" + ], + "datapoints": [ + [ + 1518134963, + 144.76668, + 21.064333, + 49.474, + 1232.0, + 340.66666, + 138.93651 + ] + ] +} diff --git a/tests/fixtures/foobot_devices.json b/tests/fixtures/foobot_devices.json new file mode 100644 index 00000000000..fffc8e151cc --- /dev/null +++ b/tests/fixtures/foobot_devices.json @@ -0,0 +1,8 @@ +[ + { + "uuid": "231425657665645342", + "userId": 6545342, + "mac": "A2D3F1", + "name": "Happybot" + } +] diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index f5415ffe212..28bb31c8482 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -8,13 +8,13 @@ import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component import homeassistant.helpers.aiohttp_client as client -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from tests.common import get_test_home_assistant @pytest.fixture -def camera_client(hass, test_client): +def camera_client(hass, aiohttp_client): """Fixture to fetch camera streams.""" assert hass.loop.run_until_complete(async_setup_component(hass, 'camera', { 'camera': { @@ -23,7 +23,7 @@ def camera_client(hass, test_client): 'mjpeg_url': 'http://example.com/mjpeg_stream', }})) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) class TestHelpersAiohttpClient(unittest.TestCase): diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 2087dc2adb5..b345400ba17 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -1,5 +1,4 @@ """Test discovery helpers.""" -import asyncio from unittest.mock import patch import pytest @@ -24,7 +23,8 @@ class TestHelpersDiscovery: """Stop everything that was started.""" self.hass.stop() - @patch('homeassistant.setup.async_setup_component') + @patch('homeassistant.setup.async_setup_component', + return_value=mock_coro()) def test_listen(self, mock_setup_component): """Test discovery listen/discover combo.""" helpers = self.hass.helpers @@ -199,15 +199,13 @@ class TestHelpersDiscovery: assert len(component_calls) == 1 -@asyncio.coroutine -def test_load_platform_forbids_config(): +async def test_load_platform_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_load_platform(None, 'config', 'zwave') + await discovery.async_load_platform(None, 'config', 'zwave') -@asyncio.coroutine -def test_discover_forbids_config(): +async def test_discover_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_discover(None, None, None, 'config') + await discovery.async_discover(None, None, None, 'config') diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a8ae20ad69b..4297ca26e7d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -218,6 +218,32 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_invalid_template(self): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + script_obj.run() + self.hass.block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index cc42bc8d7f8..f230d03e51e 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -7,7 +7,7 @@ from unittest.mock import patch import homeassistant.core as ha import homeassistant.components as core_components from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util import dt as dt_util from homeassistant.helpers import state from homeassistant.const import ( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 47e46bae3c7..def06ea9284 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -836,6 +836,12 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) "{{ is_state(trigger.entity_id, 'off') }}", {'trigger': {'entity_id': 'input_boolean.switch'}})) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'})) + @asyncio.coroutine def test_state_with_unit(hass): diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 840f665f410..c72efca8c29 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -1,11 +1,23 @@ """Test the translation helper.""" # pylint: disable=protected-access from os import path +from unittest.mock import patch +import pytest + +from homeassistant import config_entries import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component +@pytest.fixture +def mock_config_flows(): + """Mock the config flows.""" + flows = [] + with patch.object(config_entries, 'FLOWS', flows): + yield flows + + def test_flatten(): """Test the flatten function.""" data = { @@ -71,7 +83,7 @@ def test_load_translations_files(hass): } -async def test_get_translations(hass): +async def test_get_translations(hass, mock_config_flows): """Test the get translations helper.""" translations = await translation.async_get_translations(hass, 'en') assert translations == {} @@ -106,3 +118,17 @@ async def test_get_translations(hass): 'component.switch.state.string1': 'Value 1', 'component.switch.state.string2': 'Value 2', } + + +async def test_get_translations_loads_config_flows(hass, mock_config_flows): + """Test the get translations helper loads config flow translations.""" + mock_config_flows.append('component1') + + with patch.object(translation, 'component_translation_file', + return_value='bla.json'), \ + patch.object(translation, 'load_translations_files', return_value={ + 'component1': {'hello': 'world'}}): + translations = await translation.async_get_translations(hass, 'en') + assert translations == { + 'component.component1.hello': 'world' + } diff --git a/tests/mock/homekit.py b/tests/mock/homekit.py deleted file mode 100644 index 2872fa59f19..00000000000 --- a/tests/mock/homekit.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Basic mock functions and objects related to the HomeKit component.""" -PATH_HOMEKIT = 'homeassistant.components.homekit' - - -def get_patch_paths(name=None): - """Return paths to mock 'add_preload_service'.""" - path_acc = PATH_HOMEKIT + '.accessories.add_preload_service' - path_file = PATH_HOMEKIT + '.' + str(name) + '.add_preload_service' - return (path_acc, path_file) - - -def mock_preload_service(acc, service, chars=None, opt_chars=None): - """Mock alternative for function 'add_preload_service'.""" - service = MockService(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - service.add_characteristic(char_name) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - service.add_characteristic(opt_char_name) - acc.add_service(service) - return service - - -class MockAccessory(): - """Define all attributes and methods for a MockAccessory.""" - - def __init__(self, name): - """Initialize a MockAccessory object.""" - self.display_name = name - self.services = [] - - def __repr__(self): - """Return a representation of a MockAccessory. Use for debugging.""" - serv_list = [serv.display_name for serv in self.services] - return "".format( - self.display_name, serv_list) - - def add_service(self, service): - """Add service to list of services.""" - self.services.append(service) - - def get_service(self, name): - """Retrieve service from service list or return new MockService.""" - for serv in self.services: - if serv.display_name == name: - return serv - serv = MockService(name) - self.add_service(serv) - return serv - - -class MockService(): - """Define all attributes and methods for a MockService.""" - - def __init__(self, name): - """Initialize a MockService object.""" - self.characteristics = [] - self.opt_characteristics = [] - self.display_name = name - - def __repr__(self): - """Return a representation of a MockService. Use for debugging.""" - char_list = [char.display_name for char in self.characteristics] - opt_char_list = [ - char.display_name for char in self.opt_characteristics] - return "".format( - self.display_name, char_list, opt_char_list) - - def add_characteristic(self, char): - """Add characteristic to char list.""" - self.characteristics.append(char) - - def add_opt_characteristic(self, char): - """Add characteristic to opt_char list.""" - self.opt_characteristics.append(char) - - def get_characteristic(self, name): - """Get char for char lists or return new MockChar.""" - for char in self.characteristics: - if char.display_name == name: - return char - for char in self.opt_characteristics: - if char.display_name == name: - return char - char = MockChar(name) - self.add_characteristic(char) - return char - - -class MockChar(): - """Define all attributes and methods for a MockChar.""" - - def __init__(self, name): - """Initialize a MockChar object.""" - self.display_name = name - self.properties = {} - self.value = None - self.type_id = None - self.setter_callback = None - - def __repr__(self): - """Return a representation of a MockChar. Use for debugging.""" - return "".format( - self.display_name, self.value) - - def set_value(self, value, should_notify=True, should_callback=True): - """Set value of char.""" - self.value = value - if self.setter_callback is not None and should_callback: - # pylint: disable=not-callable - self.setter_callback(value) - - def get_value(self): - """Get char value.""" - return self.value - - -class MockTypeLoader(): - """Define all attributes and methods for a MockTypeLoader.""" - - def __init__(self, class_type): - """Initialize a MockTypeLoader object.""" - self.class_type = class_type - - def get(self, name): - """Return a MockService or MockChar object.""" - if self.class_type == 'service': - return MockService(name) - elif self.class_type == 'char': - return MockChar(name) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 677ed8de110..28a3f2ebdc8 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -41,9 +41,7 @@ class TestCheckConfig(unittest.TestCase): # this ensures we have one. try: asyncio.get_event_loop() - except (RuntimeError, AssertionError): - # Py35: RuntimeError - # Py34: AssertionError + except RuntimeError: asyncio.set_event_loop(asyncio.new_event_loop()) # Will allow seeing full diff diff --git a/tests/test_config.py b/tests/test_config.py index 99c21493711..652b931366a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,12 +12,13 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) from homeassistant.util import location as location_util, dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from homeassistant.components.config.group import ( CONFIG_PATH as GROUP_CONFIG_PATH) @@ -27,9 +28,9 @@ from homeassistant.components.config.script import ( CONFIG_PATH as SCRIPTS_CONFIG_PATH) from homeassistant.components.config.customize import ( CONFIG_PATH as CUSTOMIZE_CONFIG_PATH) +import homeassistant.scripts.check_config as check_config -from tests.common import ( - get_test_config_dir, get_test_home_assistant, mock_coro) +from tests.common import get_test_config_dir, get_test_home_assistant CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -235,6 +236,29 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_dict_schema(self): + """Test basic customize config validation.""" + values = ( + {ATTR_FRIENDLY_NAME: None}, + {ATTR_HIDDEN: '2'}, + {ATTR_ASSUMED_STATE: '2'}, + ) + + for val in values: + print(val) + with pytest.raises(MultipleInvalid): + config_util.CUSTOMIZE_DICT_SCHEMA(val) + + assert config_util.CUSTOMIZE_DICT_SCHEMA({ + ATTR_FRIENDLY_NAME: 2, + ATTR_HIDDEN: '1', + ATTR_ASSUMED_STATE: '0', + }) == { + ATTR_FRIENDLY_NAME: '2', + ATTR_HIDDEN: True, + ATTR_ASSUMED_STATE: False + } + def test_customize_glob_is_ordered(self): """Test that customize_glob preserves order.""" conf = config_util.CORE_CONFIG_SCHEMA( @@ -514,35 +538,25 @@ class TestConfig(unittest.TestCase): assert len(self.hass.config.whitelist_external_dirs) == 1 assert "/test/config/www" in self.hass.config.whitelist_external_dirs - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_correct(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_correct(self, mock_check): """Check that restart propagates to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': mock_coro((b'output', None)), - 'wait.return_value': mock_coro(0)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) - + mock_check.return_value = check_config.HomeAssistantConfig() assert run_coroutine_threadsafe( - config_util.async_check_ha_config_file(self.hass), self.hass.loop + config_util.async_check_ha_config_file(self.hass), + self.hass.loop ).result() is None - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_wrong(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_wrong(self, mock_check): """Check that restart with a bad config doesn't propagate to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': - mock_coro(('\033[34mhello'.encode('utf-8'), None)), - 'wait.return_value': mock_coro(1)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) + mock_check.return_value = check_config.HomeAssistantConfig() + mock_check.return_value.add_error("bad") assert run_coroutine_threadsafe( config_util.async_check_ha_config_file(self.hass), self.hass.loop - ).result() == 'hello' + ).result() == 'bad' # pylint: disable=redefined-outer-name @@ -578,6 +592,25 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None +def test_merge_try_falsy(merge_log_err): + """Ensure we dont add falsy items like empty OrderedDict() to list.""" + packages = { + 'pack_falsy_to_lst': {'automation': OrderedDict()}, + 'pack_list2': {'light': OrderedDict()}, + } + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'automation': {'do': 'something'}, + 'light': {'some': 'light'}, + } + config_util.merge_packages_config(config, packages) + + assert merge_log_err.call_count == 0 + assert len(config) == 3 + assert len(config['automation']) == 1 + assert len(config['light']) == 1 + + def test_merge_new(merge_log_err): """Test adding new components to outer scope.""" packages = { diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3a1fe1d9d3e..5b1ec3b8ec0 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -226,14 +226,14 @@ def test_configure_reuses_handler_instance(manager): def async_step_init(self, user_input=None): self.handle_count += 1 return self.async_show_form( - title=str(self.handle_count), + errors={'base': str(self.handle_count)}, step_id='init') with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): form = yield from manager.flow.async_init('test') - assert form['title'] == '1' + assert form['errors']['base'] == '1' form = yield from manager.flow.async_configure(form['flow_id']) - assert form['title'] == '2' + assert form['errors']['base'] == '2' assert len(manager.flow.async_progress()) == 1 assert len(manager.async_entries()) == 0 @@ -250,7 +250,6 @@ def test_configure_two_steps(manager): self.init_data = user_input return self.async_step_second() return self.async_show_form( - title='title', step_id='init', data_schema=vol.Schema([str]) ) @@ -263,7 +262,6 @@ def test_configure_two_steps(manager): data=self.init_data + user_input ) return self.async_show_form( - title='title', step_id='second', data_schema=vol.Schema([str]) ) @@ -299,9 +297,7 @@ def test_show_form(manager): @asyncio.coroutine def async_step_init(self, user_input=None): return self.async_show_form( - title='Hello form', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' @@ -311,8 +307,6 @@ def test_show_form(manager): with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): form = yield from manager.flow.async_init('test') assert form['type'] == 'form' - assert form['title'] == 'Hello form' - assert form['description'] == 'test-description' assert form['data_schema'] is schema assert form['errors'] == { 'username': 'Should be unique.' diff --git a/tests/test_core.py b/tests/test_core.py index 261b6385b04..1fcd9416f36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,7 +14,7 @@ import pytest import homeassistant.core as ha from homeassistant.exceptions import (InvalidEntityFormatError, InvalidStateError) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( @@ -809,7 +809,8 @@ class TestConfig(unittest.TestCase): valid = [ test_file, - tmp_dir + tmp_dir, + os.path.join(tmp_dir, 'notfound321') ] for path in valid: assert self.config.is_allowed_path(path) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d661ffba477..e67d5de50d1 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -82,7 +82,8 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" session = ClientSession(loop=loop) - session._request = self.match_request + # Setting directly on `session` will raise deprecation warning + object.__setattr__(session, '_request', self.match_request) return session async def match_request(self, method, url, *, data=None, auth=None, diff --git a/tests/testing_config/.remember_the_milk.conf b/tests/testing_config/.remember_the_milk.conf new file mode 100644 index 00000000000..272ac0903bd --- /dev/null +++ b/tests/testing_config/.remember_the_milk.conf @@ -0,0 +1 @@ +{"myprofile": {"id_map": {}}} \ No newline at end of file diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index fafe88eecbe..71625dfdf93 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -1,5 +1,5 @@ """ -Provide a mock switch platform. +Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ diff --git a/tests/util/test_async.py b/tests/util/test_async.py index b6ae58a484f..3e57ea20b5c 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -5,7 +5,7 @@ from unittest import TestCase import pytest -from homeassistant.util import async as hasync +from homeassistant.util import async_ as hasync @patch('asyncio.coroutines.iscoroutine') diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 86d303c23b7..b64cf0acf80 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -10,26 +10,52 @@ class TestColorUtil(unittest.TestCase): """Test color util methods.""" # pylint: disable=invalid-name - def test_color_RGB_to_xy(self): - """Test color_RGB_to_xy.""" - self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0)) + def test_color_RGB_to_xy_brightness(self): + """Test color_RGB_to_xy_brightness.""" + self.assertEqual((0, 0, 0), + color_util.color_RGB_to_xy_brightness(0, 0, 0)) self.assertEqual((0.32, 0.336, 255), - color_util.color_RGB_to_xy(255, 255, 255)) + color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), - color_util.color_RGB_to_xy(0, 0, 255)) + color_util.color_RGB_to_xy_brightness(0, 0, 255)) self.assertEqual((0.172, 0.747, 170), - color_util.color_RGB_to_xy(0, 255, 0)) + color_util.color_RGB_to_xy_brightness(0, 255, 0)) self.assertEqual((0.679, 0.321, 80), + color_util.color_RGB_to_xy_brightness(255, 0, 0)) + + self.assertEqual((0.679, 0.321, 17), + color_util.color_RGB_to_xy_brightness(128, 0, 0)) + + def test_color_RGB_to_xy(self): + """Test color_RGB_to_xy.""" + self.assertEqual((0, 0), + color_util.color_RGB_to_xy(0, 0, 0)) + self.assertEqual((0.32, 0.336), + color_util.color_RGB_to_xy(255, 255, 255)) + + self.assertEqual((0.136, 0.04), + color_util.color_RGB_to_xy(0, 0, 255)) + + self.assertEqual((0.172, 0.747), + color_util.color_RGB_to_xy(0, 255, 0)) + + self.assertEqual((0.679, 0.321), color_util.color_RGB_to_xy(255, 0, 0)) + self.assertEqual((0.679, 0.321), + color_util.color_RGB_to_xy(128, 0, 0)) + def test_color_xy_brightness_to_RGB(self): - """Test color_RGB_to_xy.""" + """Test color_xy_brightness_to_RGB.""" self.assertEqual((0, 0, 0), color_util.color_xy_brightness_to_RGB(1, 1, 0)) + self.assertEqual((194, 186, 169), + color_util.color_xy_brightness_to_RGB(.35, .35, 128)) + self.assertEqual((255, 243, 222), color_util.color_xy_brightness_to_RGB(.35, .35, 255)) @@ -42,6 +68,20 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0, 63, 255), color_util.color_xy_brightness_to_RGB(0, 0, 255)) + def test_color_xy_to_RGB(self): + """Test color_xy_to_RGB.""" + self.assertEqual((255, 243, 222), + color_util.color_xy_to_RGB(.35, .35)) + + self.assertEqual((255, 0, 60), + color_util.color_xy_to_RGB(1, 0)) + + self.assertEqual((0, 255, 0), + color_util.color_xy_to_RGB(0, 1)) + + self.assertEqual((0, 63, 255), + color_util.color_xy_to_RGB(0, 0)) + def test_color_RGB_to_hsv(self): """Test color_RGB_to_hsv.""" self.assertEqual((0, 0, 0), @@ -110,6 +150,23 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((225.176, 100), color_util.color_xy_to_hs(0, 0)) + def test_color_hs_to_xy(self): + """Test color_hs_to_xy.""" + self.assertEqual((0.151, 0.343), + color_util.color_hs_to_xy(180, 100)) + + self.assertEqual((0.352, 0.329), + color_util.color_hs_to_xy(350, 12.5)) + + self.assertEqual((0.228, 0.476), + color_util.color_hs_to_xy(140, 50)) + + self.assertEqual((0.465, 0.33), + color_util.color_hs_to_xy(0, 40)) + + self.assertEqual((0.32, 0.336), + color_util.color_hs_to_xy(360, 0)) + def test_rgb_hex_to_rgb_list(self): """Test rgb_hex_to_rgb_list.""" self.assertEqual([255, 255, 255], diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 5493843c246..60b0e68ca59 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -291,3 +291,11 @@ async def test_throttle_async(): assert (await test_method()) is True assert (await test_method()) is None + + @util.Throttle(timedelta(seconds=2), timedelta(seconds=0.1)) + async def test_method2(): + """Only first call should return a value.""" + return True + + assert (await test_method2()) is True + assert (await test_method2()) is None