diff --git a/.coveragerc b/.coveragerc index 84ca187fb3a..4b19519038f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,6 +145,9 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py + homeassistant/components/mercedesme.py + homeassistant/components/*/mercedesme.py + homeassistant/components/mochad.py homeassistant/components/*/mochad.py @@ -377,6 +380,7 @@ omit = homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py + homeassistant/components/goalfeed.py homeassistant/components/ifttt.py homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py @@ -435,6 +439,7 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/liveboxplaytv.py + homeassistant/components/media_player/mediaroom.py homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py @@ -449,7 +454,6 @@ omit = homeassistant/components/media_player/roku.py homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rnet.py - homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/spotify.py @@ -506,6 +510,7 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py + homeassistant/components/remote/xiaomi_miio.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py @@ -592,6 +597,7 @@ omit = homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py + homeassistant/components/sensor/pollen.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pyload.py diff --git a/.gitignore b/.gitignore index c8a6fed2ddf..0d55cae3c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,9 @@ Icon # Thumbnails ._* +# IntelliJ IDEA .idea +*.iml # pytest .cache @@ -98,3 +100,6 @@ desktop.ini /home-assistant.pyproj /home-assistant.sln /.vs/* + +# mypy +/.mypy_cache/* diff --git a/CODEOWNERS b/CODEOWNERS index 9ec7ce0742c..6e088a84e5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,7 +41,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/hassio.py @home-assistant/hassio -# Indiviudal components +# Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 @@ -61,6 +61,7 @@ homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel +homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/waqi.py @andrey-git diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html index 272809d1920..53a8d1e425d 100644 --- a/docs/source/_templates/links.html +++ b/docs/source/_templates/links.html @@ -2,5 +2,5 @@
  • Homepage
  • Community Forums
  • GitHub
  • -
  • Gitter
  • +
  • Discord
  • diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b7301e13bea..1cf6ecf7b98 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -182,7 +182,8 @@ def check_pid(pid_file: str) -> None: """Check that Home Assistant is not already running.""" # Check pid file try: - pid = int(open(pid_file, 'r').readline()) + with open(pid_file, 'r') as file: + pid = int(file.readline()) except IOError: # PID File does not exist return @@ -204,7 +205,8 @@ def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: - open(pid_file, 'w').write(str(pid)) + with open(pid_file, 'w') as file: + file.write(str(pid)) except IOError: print('Fatal Error: Unable to write pid file {}'.format(pid_file)) sys.exit(1) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 6db147a5f59..a1c6811afe7 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -133,7 +133,7 @@ def async_setup(hass, config): # have been processed. If a service does not exist it causes a 10 # second delay while we're blocking waiting for a response. # But services can be registered on other HA instances that are - # listening to the bus too. So as a in between solution, we'll + # listening to the bus too. So as an in between solution, we'll # block only if the service is defined in the current HA instance. blocking = hass.services.has_service(domain, service.service) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 27d1625fd6b..eb941e22877 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -277,7 +277,7 @@ class Alert(ToggleEntity): yield from self.async_update_ha_state() @asyncio.coroutine - def async_toggle(self): + def async_toggle(self, **kwargs): """Async toggle alert.""" if self._ack: return self.async_turn_on() diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2fae0b323a0..354a612c4b8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -17,7 +17,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description' CONF_DISPLAY_CATEGORIES = 'display_categories' HANDLERS = Registry() +ENTITY_ADAPTERS = Registry() class _DisplayCategory(object): @@ -50,8 +51,8 @@ class _DisplayCategory(object): # Describes a combination of devices set to a specific state, when the # state change must occur in a specific order. For example, a "watch - # Neflix" scene might require the: 1. TV to be powered on & 2. Input set to - # HDMI1. Applies to Scenes + # Netflix" scene might require the: 1. TV to be powered on & 2. Input set + # to HDMI1. Applies to Scenes ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER" # Indicates media devices with video or photo capabilities. @@ -133,10 +134,36 @@ def _capability(interface, return result -class _EntityCapabilities(object): +class _UnsupportedInterface(Exception): + """This entity does not support the requested Smart Home API interface.""" + + +class _UnsupportedProperty(Exception): + """This entity does not support the requested Smart Home API property.""" + + +class _AlexaEntity(object): + """An adaptation of an entity, expressed in Alexa's terms. + + The API handlers should manipulate entities only through this interface. + """ + def __init__(self, config, entity): self.config = config self.entity = entity + self.entity_conf = config.entity_config.get(entity.entity_id, {}) + + def friendly_name(self): + """Return the Alexa API friendly name.""" + return self.entity_conf.get(CONF_NAME, self.entity.name) + + def description(self): + """Return the Alexa API description.""" + return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id) + + def entity_id(self): + """Return the Alexa API entity id.""" + return self.entity.entity_id.replace('.', '#') def display_categories(self): """Return a list of display categories.""" @@ -154,17 +181,217 @@ class _EntityCapabilities(object): """ raise NotImplementedError - def capabilities(self): - """Return a list of supported capabilities. + def get_interface(self, capability): + """Return the given _AlexaInterface. - If the returned list is empty, the entity will not be discovered. + Raises _UnsupportedInterface. + """ + pass - You might find _capability() useful. + def interfaces(self): + """Return a list of supported interfaces. + + Used for discovery. The list should contain _AlexaInterface instances. + If the list is empty, this entity will not be discovered. """ raise NotImplementedError -class _GenericCapabilities(_EntityCapabilities): +class _AlexaInterface(object): + def __init__(self, entity): + self.entity = entity + + def name(self): + """Return the Alexa API name of this interface.""" + raise NotImplementedError + + @staticmethod + def properties_supported(): + """Return what properties this entity supports.""" + return [] + + @staticmethod + def properties_proactively_reported(): + """Return True if properties asynchronously reported.""" + return False + + @staticmethod + def properties_retrievable(): + """Return True if properties can be retrieved.""" + return False + + @staticmethod + def get_property(name): + """Read and return a property. + + Return value should be a dict, or raise _UnsupportedProperty. + + Properties can also have a timeOfSample and uncertaintyInMilliseconds, + but returning those metadata is not yet implemented. + """ + raise _UnsupportedProperty(name) + + @staticmethod + def supports_deactivation(): + """Applicable only to scenes.""" + return None + + def serialize_discovery(self): + """Serialize according to the Discovery API.""" + result = { + 'type': 'AlexaInterface', + 'interface': self.name(), + 'version': '3', + 'properties': { + 'supported': self.properties_supported(), + 'proactivelyReported': self.properties_proactively_reported(), + 'retrievable': self.properties_retrievable(), + }, + } + + # pylint: disable=assignment-from-none + supports_deactivation = self.supports_deactivation() + if supports_deactivation is not None: + result['supportsDeactivation'] = supports_deactivation + return result + + def serialize_properties(self): + """Return properties serialized for an API response.""" + for prop in self.properties_supported(): + prop_name = prop['name'] + yield { + 'name': prop_name, + 'namespace': self.name(), + 'value': self.get_property(prop_name), + } + + +class _AlexaPowerController(_AlexaInterface): + def name(self): + return 'Alexa.PowerController' + + def properties_supported(self): + return [{'name': 'powerState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'powerState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_ON: + return 'ON' + return 'OFF' + + +class _AlexaLockController(_AlexaInterface): + def name(self): + return 'Alexa.LockController' + + def properties_supported(self): + return [{'name': 'lockState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'lockState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_LOCKED: + return 'LOCKED' + elif self.entity.state == STATE_UNLOCKED: + return 'UNLOCKED' + return 'JAMMED' + + +class _AlexaSceneController(_AlexaInterface): + def __init__(self, entity, supports_deactivation): + _AlexaInterface.__init__(self, entity) + self.supports_deactivation = lambda: supports_deactivation + + def name(self): + return 'Alexa.SceneController' + + +class _AlexaBrightnessController(_AlexaInterface): + def name(self): + return 'Alexa.BrightnessController' + + def properties_supported(self): + return [{'name': 'brightness'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'brightness': + raise _UnsupportedProperty(name) + + return round(self.entity.attributes['brightness'] / 255.0 * 100) + + +class _AlexaColorController(_AlexaInterface): + def name(self): + return 'Alexa.ColorController' + + +class _AlexaColorTemperatureController(_AlexaInterface): + def name(self): + return 'Alexa.ColorTemperatureController' + + +class _AlexaPercentageController(_AlexaInterface): + def name(self): + return 'Alexa.PercentageController' + + +class _AlexaSpeaker(_AlexaInterface): + def name(self): + return 'Alexa.Speaker' + + +class _AlexaStepSpeaker(_AlexaInterface): + def name(self): + return 'Alexa.StepSpeaker' + + +class _AlexaPlaybackController(_AlexaInterface): + def name(self): + return 'Alexa.PlaybackController' + + +class _AlexaInputController(_AlexaInterface): + def name(self): + return 'Alexa.InputController' + + +class _AlexaTemperatureSensor(_AlexaInterface): + def name(self): + return 'Alexa.TemperatureSensor' + + def properties_supported(self): + return [{'name': 'temperature'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'temperature': + raise _UnsupportedProperty(name) + + unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + return { + 'value': float(self.entity.state), + 'scale': API_TEMP_UNITS[unit], + } + + +@ENTITY_ADAPTERS.register(alert.DOMAIN) +@ENTITY_ADAPTERS.register(automation.DOMAIN) +@ENTITY_ADAPTERS.register(input_boolean.DOMAIN) +class _GenericCapabilities(_AlexaEntity): """A generic, on/off device. The choice of last resort. @@ -173,78 +400,87 @@ class _GenericCapabilities(_EntityCapabilities): def default_display_categories(self): return [_DisplayCategory.OTHER] - def capabilities(self): - return [_capability('Alexa.PowerController')] + def interfaces(self): + return [_AlexaPowerController(self.entity)] -class _SwitchCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(switch.DOMAIN) +class _SwitchCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SWITCH] - def capabilities(self): - return [_capability('Alexa.PowerController')] + def interfaces(self): + return [_AlexaPowerController(self.entity)] -class _CoverCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(cover.DOMAIN) +class _CoverCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.DOOR] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: - capabilities.append(_capability('Alexa.PercentageController')) - return capabilities + yield _AlexaPercentageController(self.entity) -class _LightCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(light.DOMAIN) +class _LightCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.LIGHT] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & light.SUPPORT_BRIGHTNESS: - capabilities.append(_capability('Alexa.BrightnessController')) + yield _AlexaBrightnessController(self.entity) if supported & light.SUPPORT_RGB_COLOR: - capabilities.append(_capability('Alexa.ColorController')) + yield _AlexaColorController(self.entity) if supported & light.SUPPORT_XY_COLOR: - capabilities.append(_capability('Alexa.ColorController')) + yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: - capabilities.append( - _capability('Alexa.ColorTemperatureController')) - return capabilities + yield _AlexaColorTemperatureController(self.entity) -class _FanCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(fan.DOMAIN) +class _FanCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.OTHER] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: - capabilities.append(_capability('Alexa.PercentageController')) - return capabilities + yield _AlexaPercentageController(self.entity) -class _LockCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(lock.DOMAIN) +class _LockCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SMARTLOCK] - def capabilities(self): - return [_capability('Alexa.LockController')] + def interfaces(self): + return [_AlexaLockController(self.entity)] -class _MediaPlayerCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(media_player.DOMAIN) +class _MediaPlayerCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.TV] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.SUPPORT_VOLUME_SET: - capabilities.append(_capability('Alexa.Speaker')) + yield _AlexaSpeaker(self.entity) + + step_volume_features = (media_player.SUPPORT_VOLUME_MUTE | + media_player.SUPPORT_VOLUME_STEP) + if supported & step_volume_features: + yield _AlexaStepSpeaker(self.entity) playback_features = (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE | @@ -252,89 +488,62 @@ class _MediaPlayerCapabilities(_EntityCapabilities): media_player.SUPPORT_NEXT_TRACK | media_player.SUPPORT_PREVIOUS_TRACK) if supported & playback_features: - capabilities.append(_capability('Alexa.PlaybackController')) + yield _AlexaPlaybackController(self.entity) - return capabilities + if supported & media_player.SUPPORT_SELECT_SOURCE: + yield _AlexaInputController(self.entity) -class _SceneCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(scene.DOMAIN) +class _SceneCapabilities(_AlexaEntity): + def description(self): + # Required description as per Amazon Scene docs + scene_fmt = '{} (Scene connected via Home Assistant)' + return scene_fmt.format(_AlexaEntity.description(self)) + def default_display_categories(self): return [_DisplayCategory.SCENE_TRIGGER] - def capabilities(self): - return [_capability('Alexa.SceneController')] + def interfaces(self): + return [_AlexaSceneController(self.entity, + supports_deactivation=False)] -class _ScriptCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(script.DOMAIN) +class _ScriptCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.ACTIVITY_TRIGGER] - def capabilities(self): + def interfaces(self): can_cancel = bool(self.entity.attributes.get('can_cancel')) - return [_capability('Alexa.SceneController', - supports_deactivation=can_cancel)] + return [_AlexaSceneController(self.entity, + supports_deactivation=can_cancel)] -class _GroupCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(group.DOMAIN) +class _GroupCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SCENE_TRIGGER] - def capabilities(self): - return [_capability('Alexa.SceneController', - supports_deactivation=True)] + def interfaces(self): + return [_AlexaSceneController(self.entity, + supports_deactivation=True)] -class _SensorCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(sensor.DOMAIN) +class _SensorCapabilities(_AlexaEntity): def default_display_categories(self): # although there are other kinds of sensors, all but temperature # sensors are currently ignored. return [_DisplayCategory.TEMPERATURE_SENSOR] - def capabilities(self): - capabilities = [] - + def interfaces(self): attrs = self.entity.attributes if attrs.get(CONF_UNIT_OF_MEASUREMENT) in ( TEMP_FAHRENHEIT, TEMP_CELSIUS, ): - capabilities.append(_capability( - 'Alexa.TemperatureSensor', - retrievable=True, - properties_supported=[{'name': 'temperature'}])) - - return capabilities - - -class _UnknownEntityDomainError(Exception): - pass - - -def _capabilities_for_entity(config, entity): - """Return an _EntityCapabilities appropriate for given entity. - - raises _UnknownEntityDomainError if the given domain is unsupported. - """ - if entity.domain not in _CAPABILITIES_FOR_DOMAIN: - raise _UnknownEntityDomainError() - return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity) - - -_CAPABILITIES_FOR_DOMAIN = { - alert.DOMAIN: _GenericCapabilities, - automation.DOMAIN: _GenericCapabilities, - cover.DOMAIN: _CoverCapabilities, - fan.DOMAIN: _FanCapabilities, - group.DOMAIN: _GroupCapabilities, - input_boolean.DOMAIN: _GenericCapabilities, - light.DOMAIN: _LightCapabilities, - lock.DOMAIN: _LockCapabilities, - media_player.DOMAIN: _MediaPlayerCapabilities, - scene.DOMAIN: _SceneCapabilities, - script.DOMAIN: _ScriptCapabilities, - switch.DOMAIN: _SwitchCapabilities, - sensor.DOMAIN: _SensorCapabilities, -} + yield _AlexaTemperatureSensor(self.entity) class _Cause(object): @@ -468,7 +677,7 @@ def api_message(request, } } - # If a correlation token exsits, add it to header / Need by Async requests + # If a correlation token exists, add it to header / Need by Async requests token = request[API_HEADER].get('correlationToken') if token: response[API_EVENT][API_HEADER]['correlationToken'] = token @@ -511,36 +720,26 @@ def async_api_discovery(hass, config, request): entity.entity_id) continue - try: - entity_capabilities = _capabilities_for_entity(config, entity) - except _UnknownEntityDomainError: + if entity.domain not in ENTITY_ADAPTERS: continue - - entity_conf = config.entity_config.get(entity.entity_id, {}) - - friendly_name = entity_conf.get(CONF_NAME, entity.name) - description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id) - - # Required description as per Amazon Scene docs - if entity.domain == scene.DOMAIN: - scene_fmt = '{} (Scene connected via Home Assistant)' - description = scene_fmt.format(description) + alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity) endpoint = { - 'displayCategories': entity_capabilities.display_categories(), + 'displayCategories': alexa_entity.display_categories(), 'additionalApplianceDetails': {}, - 'endpointId': entity.entity_id.replace('.', '#'), - 'friendlyName': friendly_name, - 'description': description, + 'endpointId': alexa_entity.entity_id(), + 'friendlyName': alexa_entity.friendly_name(), + 'description': alexa_entity.description(), 'manufacturerName': 'Home Assistant', } - alexa_capabilities = entity_capabilities.capabilities() - if not alexa_capabilities: + endpoint['capabilities'] = [ + i.serialize_discovery() for i in alexa_entity.interfaces()] + + if not endpoint['capabilities']: _LOGGER.debug("Not exposing %s because it has no capabilities", entity.entity_id) continue - endpoint['capabilities'] = alexa_capabilities discovery_endpoints.append(endpoint) return api_message( @@ -624,7 +823,7 @@ def async_api_set_brightness(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_adjust_brightness(hass, config, request, entity): - """Process a adjust brightness request.""" + """Process an adjust brightness request.""" brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) # read current state @@ -812,7 +1011,7 @@ def async_api_set_percentage(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_adjust_percentage(hass, config, request, entity): - """Process a adjust percentage request.""" + """Process an adjust percentage request.""" percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) service = None data = {ATTR_ENTITY_ID: entity.entity_id} @@ -873,7 +1072,7 @@ def async_api_lock(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_unlock(hass, config, request, entity): - """Process a unlock request.""" + """Process an unlock request.""" yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { ATTR_ENTITY_ID: entity.entity_id }, blocking=False) @@ -900,11 +1099,46 @@ def async_api_set_volume(hass, config, request, entity): return api_message(request) +@HANDLERS.register(('Alexa.InputController', 'SelectInput')) +@extract_entity +@asyncio.coroutine +def async_api_select_input(hass, config, request, entity): + """Process a set input request.""" + media_input = request[API_PAYLOAD]['input'] + + # attempt to map the ALL UPPERCASE payload name to a source + source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or [] + for source in source_list: + # response will always be space separated, so format the source in the + # most likely way to find a match + formatted_source = source.lower().replace('-', ' ').replace('_', ' ') + if formatted_source in media_input.lower(): + media_input = source + break + else: + msg = 'failed to map input {} to a media source on {}'.format( + media_input, entity.entity_id) + _LOGGER.error(msg) + return api_error( + request, error_type='INVALID_VALUE', error_message=msg) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_INPUT_SOURCE: media_input, + } + + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_SELECT_SOURCE, + data, blocking=False) + + return api_message(request) + + @HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) @extract_entity @asyncio.coroutine def async_api_adjust_volume(hass, config, request, entity): - """Process a adjust volume request.""" + """Process an adjust volume request.""" volume_delta = int(request[API_PAYLOAD]['volume']) current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) @@ -929,6 +1163,30 @@ def async_api_adjust_volume(hass, config, request, entity): return api_message(request) +@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_volume_step(hass, config, request, entity): + """Process an adjust volume step request.""" + volume_step = round(float(request[API_PAYLOAD]['volume'] / 100), 2) + + current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + volume = current_level + volume_step + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_SET, + data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute')) @HANDLERS.register(('Alexa.Speaker', 'SetMute')) @extract_entity @asyncio.coroutine @@ -1033,18 +1291,13 @@ def async_api_previous(hass, config, request, entity): @asyncio.coroutine def async_api_reportstate(hass, config, request, entity): """Process a ReportState request.""" - unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] - temp_property = { - 'namespace': 'Alexa.TemperatureSensor', - 'name': 'temperature', - 'value': { - 'value': float(entity.state), - 'scale': API_TEMP_UNITS[unit], - }, - } + alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity) + properties = [] + for interface in alexa_entity.interfaces(): + properties.extend(interface.serialize_properties()) return api_message( request, name='StateReport', - context={'properties': [temp_property]} + context={'properties': properties} ) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 2883fca9ab6..5fbd5a764e9 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -251,7 +251,7 @@ class AndroidIPCamEntity(Entity): """The Android device running IP Webcam.""" def __init__(self, host, ipcam): - """Initialize the data oject.""" + """Initialize the data object.""" self._host = host self._ipcam = ipcam diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index dd29e7d602f..7e2b4cda28f 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -66,7 +66,7 @@ class APCUPSdData(object): """ def __init__(self, host, port): - """Initialize the data oject.""" + """Initialize the data object.""" from apcaccess import status self._host = host self._port = port diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index a928ed108c9..7e51ec8c045 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -47,7 +47,7 @@ def setup(hass, config): return False hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex)) + _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) hass.components.persistent_notification.create( 'Error: {}
    ' 'You will need to restart hass after fixing.' diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 5e69dcc9109..1d0849b255e 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name) self._state = None @property @@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 3c02dfb3508..0d7c3e086bb 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -65,6 +65,11 @@ class DeconzBinarySensor(BinarySensorDevice): """Return the name of the sensor.""" return self._sensor.name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return self._sensor.uniqueid + @property def device_class(self): """Return the class of the sensor.""" diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py index 214efb870b9..15efa21b226 100644 --- a/homeassistant/components/binary_sensor/ecobee.py +++ b/homeassistant/components/binary_sensor/ecobee.py @@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice): """Return the status of the sensor.""" return self._state == 'true' - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "binary_sensor_ecobee_{}_{}".format(self._name, self.index) - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 47b1be988bf..75a9fa1d046 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the FFmpeg binary moition sensor.""" + """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] if not manager.async_run_test(config.get(CONF_INPUT)): diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index df488cc0ed6..ec64bdf07b8 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -118,7 +118,7 @@ class HikvisionData(object): """Hikvision device event stream object.""" def __init__(self, hass, url, port, name, username, password): - """Initialize the data oject.""" + """Initialize the data object.""" from pyhik.hikvision import HikCamera self._url = url self._port = port @@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return an unique ID.""" - return '{}.{}'.format(self.__class__, self._id) + return self._id @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py index 6223ebe50a1..2d4cbd8d070 100644 --- a/homeassistant/components/binary_sensor/hive.py +++ b/homeassistant/components/binary_sensor/hive.py @@ -59,5 +59,5 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.node_device_type) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py index 14e45f88cf1..04f8c0d00dd 100644 --- a/homeassistant/components/binary_sensor/ihc.py +++ b/homeassistant/components/binary_sensor/ihc.py @@ -69,7 +69,8 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - sensor_type: str, inverting: bool, product: Element=None): + sensor_type: str, inverting: bool, + product: Element=None) -> None: """Initialize the IHC binary sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 0702ce8bb9e..1874be6ec41 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -83,5 +83,5 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice): @callback def async_binarysensor_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from PLM for %s", self._address) + _LOGGER.info("Received update callback from PLM for %s", self._address) self._hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 89d9b7e5c8f..4dddb9bdbef 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -67,8 +67,8 @@ def setup_platform(hass, config: ConfigType, elif subnode_id == 2: parent_device.add_negative_node(node) elif device_type == 'moisture': - # Moisure nodes have a subnode 2, but we ignore it because it's - # just the inverse of the primary node. + # Moisture nodes have a subnode 2, but we ignore it because + # it's just the inverse of the primary node. if subnode_id == 4: # Heartbeat node device = ISYBinarySensorHeartbeat(node, parent_device) diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py new file mode 100644 index 00000000000..a6c8da56ce8 --- /dev/null +++ b/homeassistant/components/binary_sensor/mercedesme.py @@ -0,0 +1,94 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, BINARY_SENSORS) + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + data = hass.data[DATA_MME].data + + if not data.cars: + _LOGGER.error("No cars found. Check component log.") + return + + devices = [] + for car in data.cars: + for key, value in sorted(BINARY_SENSORS.items()): + devices.append(MercedesMEBinarySensor( + data, key, value[0], car["vin"], None)) + + add_devices(devices, True) + + +class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): + """Representation of a Sensor.""" + + @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.""" + if self._internal_name == "windowsClosed": + return { + "window_front_left": self._car["windowStatusFrontLeft"], + "window_front_right": self._car["windowStatusFrontRight"], + "window_rear_left": self._car["windowStatusRearLeft"], + "window_rear_right": self._car["windowStatusRearRight"], + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + elif self._internal_name == "tireWarningLight": + return { + "front_right_tire_pressure_kpa": + self._car["frontRightTirePressureKpa"], + "front_left_tire_pressure_kpa": + self._car["frontLeftTirePressureKpa"], + "rear_right_tire_pressure_kpa": + self._car["rearRightTirePressureKpa"], + "rear_left_tire_pressure_kpa": + self._car["rearLeftTirePressureKpa"], + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"], + } + return { + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + def update(self): + """Fetch new state data for the sensor.""" + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "windowsClosed": + self._state = bool(self._car[self._internal_name] == "CLOSED") + elif self._internal_name == "tireWarningLight": + self._state = bool(self._car[self._internal_name] != "INACTIVE") + else: + self._state = self._car[self._internal_name] is True + + _LOGGER.debug("Updated %s Value: %s IsOn: %s", + self._internal_name, self._state, self.is_on) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 983c879338d..e033355f655 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -14,8 +14,8 @@ import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, - CONF_DEVICE_CLASS) + CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, + CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) @@ -24,8 +24,10 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'MQTT Binary sensor' + DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_FORCE_UPDATE = False DEPENDENCIES = ['mqtt'] @@ -34,6 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -53,6 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_DEVICE_CLASS), config.get(CONF_QOS), + config.get(CONF_FORCE_UPDATE), config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_AVAILABLE), @@ -65,7 +69,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, - qos, payload_on, payload_off, payload_available, + qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template): """Initialize the MQTT binary sensor.""" super().__init__(availability_topic, qos, payload_available, @@ -77,6 +81,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._payload_on = payload_on self._payload_off = payload_off self._qos = qos + self._force_update = force_update self._template = value_template @asyncio.coroutine @@ -94,6 +99,11 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._state = True elif payload == self._payload_off: self._state = False + else: # Payload is not for this entity + _LOGGER.warning('No matching payload found' + ' for entity: %s with state_topic: %s', + self._name, self._state_topic) + return self.async_schedule_update_ha_state() @@ -119,3 +129,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): def device_class(self): """Return the class of this sensor.""" return self._device_class + + @property + def force_update(self): + """Force update.""" + return self._force_update diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index e597f1d0bbe..4d8aaa7d0d9 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -131,10 +131,8 @@ class NetatmoBinarySensor(BinarySensorDevice): self._name += ' / ' + module_name self._sensor_name = sensor self._name += ' ' + sensor - camera_id = data.camera_data.cameraByName( + self._unique_id = data.camera_data.cameraByName( camera=camera_name, home=home)['id'] - self._unique_id = "Netatmo_binary_sensor {0} - {1}".format( - self._name, camera_id) self._cameratype = camera_type self._state = None diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py index c4c26d3a122..d2c46c795a8 100644 --- a/homeassistant/components/binary_sensor/pilight.py +++ b/homeassistant/components/binary_sensor/pilight.py @@ -97,7 +97,7 @@ class PilightBinarySensor(BinarySensorDevice): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ # Check if received code matches defined playoad @@ -162,10 +162,10 @@ class PilightTriggerSensor(BinarySensorDevice): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ - # Check if received code matches defined playoad + # Check if received code matches defined payload # True if payload is contained in received code dict payload_ok = True for key in self._payload: diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py index f75f7644c4e..288b46c2370 100644 --- a/homeassistant/components/binary_sensor/raincloud.py +++ b/homeassistant/components/binary_sensor/raincloud.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_type)) else: - # create an sensor for each zone managed by faucet + # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append(RainCloudBinarySensor(zone, sensor_type)) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 92213a9b590..68ffbf77af2 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, + CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -29,6 +30,8 @@ CONF_DELAY_OFF = 'delay_off' SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -38,11 +41,6 @@ SENSOR_SCHEMA = vol.Schema({ vol.All(cv.time_period, cv.positive_timedelta), }) -SENSOR_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SENSOR_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) @@ -55,6 +53,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): value_template = device_config[CONF_VALUE_TEMPLATE] + 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 value_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) @@ -65,10 +66,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass + if icon_template is not None: + icon_template.hass = hass + + if entity_picture_template is not None: + entity_picture_template.hass = hass + sensors.append( BinarySensorTemplate( hass, device, friendly_name, device_class, value_template, - entity_ids, delay_on, delay_off) + icon_template, entity_picture_template, entity_ids, + delay_on, delay_off) ) if not sensors: _LOGGER.error("No sensors added") @@ -82,7 +90,8 @@ class BinarySensorTemplate(BinarySensorDevice): """A virtual binary sensor that triggers from another sensor.""" def __init__(self, hass, device, friendly_name, device_class, - value_template, entity_ids, delay_on, delay_off): + value_template, icon_template, entity_picture_template, + entity_ids, delay_on, delay_off): """Initialize the Template binary sensor.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -91,6 +100,10 @@ class BinarySensorTemplate(BinarySensorDevice): self._device_class = device_class self._template = value_template self._state = None + self._icon_template = icon_template + self._entity_picture_template = entity_picture_template + self._icon = None + self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off @@ -119,6 +132,16 @@ class BinarySensorTemplate(BinarySensorDevice): """Return the name of the sensor.""" return self._name + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def entity_picture(self): + """Return the entity_picture to use in the frontend, if any.""" + return self._entity_picture + @property def is_on(self): """Return true if sensor is on.""" @@ -137,8 +160,9 @@ class BinarySensorTemplate(BinarySensorDevice): @callback def _async_render(self): """Get the state of template.""" + state = None try: - return self._template.async_render().lower() == 'true' + state = (self._template.async_render().lower() == 'true') except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): @@ -148,6 +172,29 @@ class BinarySensorTemplate(BinarySensorDevice): return _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)): + if template is None: + continue + + try: + setattr(self, property_name, template.async_render()) + except TemplateError as ex: + friendly_property_name = property_name[1:].replace('_', ' ') + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning('Could not render %s template %s,' + ' the state is unknown.', + friendly_property_name, self._name) + else: + _LOGGER.error('Could not render %s template %s: %s', + friendly_property_name, self._name, ex) + return state + + return state + @callback def async_check_state(self): """Update the state from the template.""" diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 36e8868661d..79c36fb2ef2 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -126,11 +126,12 @@ class ThresholdSensor(BinarySensorDevice): @property def threshold_type(self): """Return the type of threshold this sensor represents.""" - if self._threshold_lower and self._threshold_upper: + if self._threshold_lower is not None and \ + self._threshold_upper is not None: return TYPE_RANGE - elif self._threshold_lower: + elif self._threshold_lower is not None: return TYPE_LOWER - elif self._threshold_upper: + elif self._threshold_upper is not None: return TYPE_UPPER @property diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 1ec9e703eab..cc1f602d871 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -58,11 +58,11 @@ class WemoBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return the id of this WeMo device.""" - return '{}.{}'.format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): - """Return the name of the sevice if any.""" + """Return the name of the service if any.""" return self.wemo.name @property diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index ad7c29badf9..de7896e595b 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if discovery_info is None: return - from bellows.zigbee.zcl.clusters.security import IasZone + from zigpy.zcl.clusters.security import IasZone in_clusters = discovery_info['in_clusters'] @@ -63,7 +63,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) self._device_class = device_class - from bellows.zigbee.zcl.clusters.security import IasZone + from zigpy.zcl.clusters.security import IasZone self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] @property @@ -78,7 +78,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class - def cluster_command(self, aps_frame, tsn, command_id, args): + def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" if command_id == 0: self._state = args[0] & 3 diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 8b2401aa589..ba798ce7902 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -174,7 +174,7 @@ class WebDavCalendarData(object): @staticmethod def is_matching(vevent, search): - """Return if the event matches the filter critera.""" + """Return if the event matches the filter criteria.""" if search is None: return True diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 81191e3025e..f1c80612f3b 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -411,7 +411,7 @@ class TodoistProjectData(object): The "best" event is determined by the following criteria: * A proposed event must not be completed - * A proposed event must have a end date (otherwise we go with + * A proposed event must have an end date (otherwise we go with the event at index 0, selected above) * A proposed event must be on the same day or earlier as our current event diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1bb88050b2f..a531d25841b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -91,13 +91,13 @@ def async_snapshot(hass, filename, entity_id=None): @bind_hass @asyncio.coroutine def async_get_image(hass, entity_id, timeout=10): - """Fetch a image from a camera entity.""" + """Fetch an image from a camera entity.""" websession = async_get_clientsession(hass) state = hass.states.get(entity_id) if state is None: raise HomeAssistantError( - "No entity '{0}' for grab a image".format(entity_id)) + "No entity '{0}' for grab an image".format(entity_id)) url = "{0}{1}".format( hass.config.api.base_url, diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/camera/abode.py index 3c0c0a54e0e..ee739810a61 100644 --- a/homeassistant/components/camera/abode.py +++ b/homeassistant/components/camera/abode.py @@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discoveryy_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Abode camera devices.""" import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py index 302758eee94..a230e0f6d4a 100644 --- a/homeassistant/components/camera/canary.py +++ b/homeassistant/components/camera/canary.py @@ -4,19 +4,30 @@ Support for Canary camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.canary/ """ +import asyncio import logging +from datetime import timedelta -import requests +import voluptuous as vol -from homeassistant.components.camera import Camera +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.util import Throttle -DEPENDENCIES = ['canary'] +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +DEPENDENCIES = ['canary', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) -ATTR_MOTION_START_TIME = "motion_start_time" -ATTR_MOTION_END_TIME = "motion_end_time" +MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, +}) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -25,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for location in data.locations: - entries = data.get_motion_entries(location.location_id) - if entries: - devices.append(CanaryCamera(data, location.location_id, - DEFAULT_TIMEOUT)) + for device in location.devices: + if device.is_online: + devices.append( + CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT, + config.get(CONF_FFMPEG_ARGUMENTS))) add_devices(devices, True) @@ -36,60 +48,65 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CanaryCamera(Camera): """An implementation of a Canary security camera.""" - def __init__(self, data, location_id, timeout): + def __init__(self, hass, data, location, device, timeout, ffmpeg_args): """Initialize a Canary security camera.""" super().__init__() + + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = ffmpeg_args self._data = data - self._location_id = location_id + self._location = location + self._device = device self._timeout = timeout - - self._location = None - self._motion_entry = None - self._image_content = None - - def camera_image(self): - """Update the status of the camera and return bytes of camera image.""" - self.update() - return self._image_content + self._live_stream_session = None @property def name(self): """Return the name of this device.""" - return self._location.name + return self._device.name @property def is_recording(self): """Return true if the device is recording.""" return self._location.is_recording - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - if self._motion_entry is None: - return None - - return { - ATTR_MOTION_START_TIME: self._motion_entry.start_time, - ATTR_MOTION_END_TIME: self._motion_entry.end_time, - } - - def update(self): - """Update the status of the camera.""" - self._data.update() - self._location = self._data.get_location(self._location_id) - - entries = self._data.get_motion_entries(self._location_id) - if entries: - current = entries[0] - previous = self._motion_entry - - if previous is None or previous.entry_id != current.entry_id: - self._motion_entry = current - self._image_content = requests.get( - current.thumbnails[0].image_url, - timeout=self._timeout).content - @property def motion_detection_enabled(self): """Return the camera motion detection status.""" return not self._location.is_recording + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + self.renew_live_stream_session() + + from haffmpeg import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + image = yield from asyncio.shield(ffmpeg.get_image( + self._live_stream_session.live_stream_url, + output_format=IMAGE_JPEG, + extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + if self._live_stream_session is None: + return + + from haffmpeg import CameraMjpeg + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + yield from stream.open_camera( + self._live_stream_session.live_stream_url, + extra_cmd=self._ffmpeg_arguments) + + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() + + @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) + def renew_live_stream_session(self): + """Renew live stream session.""" + self._live_stream_session = self._data.get_live_stream_session( + self._device) diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index c1ec2db0a08..0a9a3fbdca4 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -64,13 +64,11 @@ class NetatmoCamera(Camera): self._name = home + ' / ' + camera_name else: self._name = camera_name - camera_id = data.camera_data.cameraByName( - camera=camera_name, home=home)['id'] - self._unique_id = "Welcome_camera {0} - {1}".format( - self._name, camera_id) self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( camera=camera_name ) + self._unique_id = data.camera_data.cameraByName( + camera=camera_name, home=home)['id'] self._cameratype = camera_type def camera_image(self): @@ -117,5 +115,5 @@ class NetatmoCamera(Camera): @property def unique_id(self): - """Return the unique ID for this sensor.""" + """Return the unique ID for this camera.""" return self._unique_id diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index 6e3d3622a3f..f37e7778414 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -28,7 +28,7 @@ CONF_VERTICAL_FLIP = 'vertical_flip' DEFAULT_HORIZONTAL_FLIP = 0 DEFAULT_IMAGE_HEIGHT = 480 -DEFAULT_IMAGE_QUALITIY = 7 +DEFAULT_IMAGE_QUALITY = 7 DEFAULT_IMAGE_ROTATION = 0 DEFAULT_IMAGE_WIDTH = 640 DEFAULT_NAME = 'Raspberry Pi Camera' @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): vol.Coerce(int), - vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY): + vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION): vol.All(vol.Coerce(int), vol.Range(min=0, max=359)), @@ -131,7 +131,7 @@ class RaspberryCamera(Camera): stderr=subprocess.STDOUT) def camera_image(self): - """Return raspstill image response.""" + """Return raspistill image response.""" with open(self._config[CONF_FILE_PATH], 'rb') as file: return file.read() diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 8d79fa04a9a..f7dc4cfd973 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -127,6 +127,9 @@ class UnifiVideoCamera(Camera): else: client_cls = uvc_camera.UVCCameraClient + if caminfo['username'] is None: + caminfo['username'] = 'ubnt' + camera = None for addr in addrs: try: diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py old mode 100755 new mode 100644 index 72f40cb83a4..b4bcad0064d --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['pyxeoma==1.2'] +REQUIREMENTS = ['pyxeoma==1.3'] _LOGGER = logging.getLogger(__name__) @@ -22,6 +22,8 @@ CONF_CAMERAS = 'cameras' CONF_HIDE = 'hide' CONF_IMAGE_NAME = 'image_name' CONF_NEW_VERSION = 'new_version' +CONF_VIEWER_PASSWORD = 'viewer_password' +CONF_VIEWER_USERNAME = 'viewer_username' CAMERAS_SCHEMA = vol.Schema({ vol.Required(CONF_IMAGE_NAME): cv.string, @@ -48,9 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host = config[CONF_HOST] login = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - new_version = config[CONF_NEW_VERSION] - xeoma = Xeoma(host, new_version, login, password) + xeoma = Xeoma(host, login, password) try: yield from xeoma.async_test_connection() @@ -59,9 +60,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): { CONF_IMAGE_NAME: image_name, CONF_HIDE: False, - CONF_NAME: image_name + CONF_NAME: image_name, + CONF_VIEWER_USERNAME: username, + CONF_VIEWER_PASSWORD: pw + } - for image_name in discovered_image_names + for image_name, username, pw in discovered_image_names ] for cam in config[CONF_CAMERAS]: @@ -77,8 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras)) async_add_devices( - [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME]) - for camera in cameras]) + [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME], + camera[CONF_VIEWER_USERNAME], + camera[CONF_VIEWER_PASSWORD]) for camera in cameras]) except XeomaError as err: _LOGGER.error("Error: %s", err.message) return @@ -87,12 +92,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class XeomaCamera(Camera): """Implementation of a Xeoma camera.""" - def __init__(self, xeoma, image, name): + def __init__(self, xeoma, image, name, username, password): """Initialize a Xeoma camera.""" super().__init__() self._xeoma = xeoma self._name = name self._image = image + self._username = username + self._password = password self._last_image = None @asyncio.coroutine @@ -100,7 +107,8 @@ class XeomaCamera(Camera): """Return a still image response from the camera.""" from pyxeoma.xeoma import XeomaError try: - image = yield from self._xeoma.async_get_camera_image(self._image) + image = yield from self._xeoma.async_get_camera_image( + self._image, self._username, self._password) self._last_image = image except XeomaError as err: _LOGGER.error("Error fetching image: %s", err.message) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 8ab7218e201..dfef4976eb8 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ['py-canary==0.2.3'] +REQUIREMENTS = ['py-canary==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -111,7 +111,18 @@ class CanaryData(object): """Return a list of readings based on device_id.""" return self._readings_by_device_id.get(device_id, []) + def get_reading(self, device_id, sensor_type): + """Return reading for device_id and sensor type.""" + readings = self._readings_by_device_id.get(device_id, []) + return next(( + reading.value for reading in readings + if reading.sensor_type == sensor_type), None) + def set_location_mode(self, location_id, mode_name, is_private=False): """Set location mode.""" self._api.set_location_mode(location_id, mode_name, is_private) self.update(no_throttle=True) + + def get_live_stream_session(self, device): + """Return live stream session.""" + return self._api.get_live_stream_session(device) diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index fea1fcee3a3..0ed4ebe8942 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -183,11 +183,6 @@ class DaikinClimate(ClimateDevice): self._force_refresh = True self._api.device.set(values) - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index c3ba523468f..102155babea 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -14,23 +14,19 @@ from homeassistant.components.climate import ( SUPPORT_ON_OFF) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | - SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | - SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | - SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF) +SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo climate devices.""" add_devices([ DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77, - 'Auto Low', None, None, 'Auto', 'heat', None, None, None), + None, None, None, None, 'heat', None, None, + None, True), DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High', - 67, 54, 'Off', 'cool', False, None, None), - DemoClimate('Ecobee', None, TEMP_CELSIUS, None, None, 23, 'Auto Low', - None, None, 'Auto', 'auto', None, 24, 21) + 67, 54, 'Off', 'cool', False, None, None, None), + DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low', + None, None, 'Auto', 'auto', None, 24, 21, None) ]) @@ -40,9 +36,37 @@ class DemoClimate(ClimateDevice): def __init__(self, name, target_temperature, unit_of_measurement, away, hold, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, - current_operation, aux, target_temp_high, target_temp_low): + current_operation, aux, target_temp_high, target_temp_low, + is_on): """Initialize the climate device.""" self._name = name + self._support_flags = SUPPORT_FLAGS + if target_temperature is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE + if away is not None: + self._support_flags = self._support_flags | SUPPORT_AWAY_MODE + if hold is not None: + self._support_flags = self._support_flags | SUPPORT_HOLD_MODE + if current_fan_mode is not None: + self._support_flags = self._support_flags | SUPPORT_FAN_MODE + if target_humidity is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_HUMIDITY + if current_swing_mode is not None: + self._support_flags = self._support_flags | SUPPORT_SWING_MODE + if current_operation is not None: + self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE + if aux is not None: + self._support_flags = self._support_flags | SUPPORT_AUX_HEAT + if target_temp_high is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE_HIGH + if target_temp_low is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE_LOW + if is_on is not None: + self._support_flags = self._support_flags | SUPPORT_ON_OFF self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement @@ -59,12 +83,12 @@ class DemoClimate(ClimateDevice): self._swing_list = ['Auto', '1', '2', '3', 'Off'] self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low - self._on = True + self._on = is_on @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @property def should_poll(self): @@ -207,7 +231,7 @@ class DemoClimate(ClimateDevice): self.schedule_update_ha_state() def turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" self._aux = True self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index b0685b337be..6a4253ceca7 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -13,7 +13,9 @@ from homeassistant.components.climate import ( DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, 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_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -46,7 +48,9 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | - SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH) + SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py index bb92a92467a..0591178391a 100644 --- a/homeassistant/components/climate/econet.py +++ b/homeassistant/components/climate/econet.py @@ -18,7 +18,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.4'] +REQUIREMENTS = ['pyeconet==0.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 9b3b7d650a9..9c712c632e6 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): - """Representation of a eQ-3 Bluetooth Smart thermostat.""" + """Representation of an eQ-3 Bluetooth Smart thermostat.""" def __init__(self, _mac, _name): """Initialize the thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 9445fc7cfc9..c66e611c8e9 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -156,7 +156,7 @@ class GenericThermostat(ClimateDevice): # If we have no initial temperature, restore if self._target_temp is None: # If we have a previously saved temperature - if old_state.attributes[ATTR_TEMPERATURE] is None: + if old_state.attributes.get(ATTR_TEMPERATURE) is None: if self.ac_mode: self._target_temp = self.max_temp else: @@ -166,15 +166,15 @@ class GenericThermostat(ClimateDevice): else: self._target_temp = float( old_state.attributes[ATTR_TEMPERATURE]) - if old_state.attributes[ATTR_AWAY_MODE] is not None: + if old_state.attributes.get(ATTR_AWAY_MODE) is not None: self._is_away = str( old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON if (self._initial_operation_mode is None and old_state.attributes[ATTR_OPERATION_MODE] is not None): self._current_operation = \ old_state.attributes[ATTR_OPERATION_MODE] - if self._current_operation != STATE_OFF: - self._enabled = True + self._enabled = self._current_operation != STATE_OFF + else: # No previous state, try and restore defaults if self._target_temp is None: @@ -249,7 +249,7 @@ class GenericThermostat(ClimateDevice): else: _LOGGER.error("Unrecognized operation mode: %s", operation_mode) return - # Ensure we updae the current operation after changing the mode + # Ensure we update the current operation after changing the mode self.schedule_update_ha_state() @asyncio.coroutine diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index b05c880cc37..19c033a319f 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): serport = connection.connection(ipaddress, port) serport.open() - for thermostat, tstat in tstats.items(): + for tstat in tstats.values(): add_devices([ HeatmiserV3Thermostat( heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport) diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index b8ac66d91b3..760ef131049 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -174,5 +174,5 @@ class HiveClimateEntity(ClimateDevice): entity.handle_update(self.data_updatesource) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py new file mode 100644 index 00000000000..2b3b3bfbab1 --- /dev/null +++ b/homeassistant/components/climate/melissa.py @@ -0,0 +1,259 @@ +""" +Support for Melissa Climate A/C. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.melissa/ +""" +import logging + +from homeassistant.components.climate import ( + ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, SUPPORT_FAN_MODE +) +from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_IDLE, ATTR_TEMPERATURE, + PRECISION_WHOLE +) + +DEPENDENCIES = ['melissa'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = (SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE | + SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE) + +OP_MODES = [ + STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT +] + +FAN_MODES = [ + STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM +] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through and add all Melissa devices.""" + api = hass.data[DATA_MELISSA] + devices = api.fetch_devices().values() + + all_devices = [] + + for device in devices: + all_devices.append(MelissaClimate( + api, device['serial_number'], device)) + + add_devices(all_devices) + + +class MelissaClimate(ClimateDevice): + """Representation of a Melissa Climate device.""" + + def __init__(self, api, serial_number, init_data): + """Initialize the climate device.""" + self._name = init_data['name'] + self._api = api + self._serial_number = serial_number + self._data = init_data['controller_log'] + self._state = None + self._cur_settings = None + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._name + + @property + def is_on(self): + """Return current state.""" + if self._cur_settings is not None: + return self._cur_settings[self._api.STATE] in ( + self._api.STATE_ON, self._api.STATE_IDLE) + return None + + @property + def current_fan_mode(self): + """Return the current fan mode.""" + if self._cur_settings is not None: + return self.melissa_fan_to_hass( + self._cur_settings[self._api.FAN]) + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._data: + return self._data[self._api.TEMP] + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return PRECISION_WHOLE + + @property + def current_operation(self): + """Return the current operation mode.""" + if self._cur_settings is not None: + return self.melissa_op_to_hass( + self._cur_settings[self._api.MODE]) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return OP_MODES + + @property + def fan_list(self): + """List of available fan modes.""" + return FAN_MODES + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._cur_settings is not None: + return self._cur_settings[self._api.TEMP] + + @property + def state(self): + """Return current state.""" + if self._cur_settings is not None: + return self.melissa_state_to_hass( + self._cur_settings[self._api.STATE]) + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum supported temperature for the thermostat.""" + return 16 + + @property + def max_temp(self): + """Return the maximum supported temperature for the thermostat.""" + return 30 + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + self.send({self._api.TEMP: temp}) + + def set_fan_mode(self, fan): + """Set fan mode.""" + fan_mode = self.hass_fan_to_melissa(fan) + self.send({self._api.FAN: fan_mode}) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + mode = self.hass_mode_to_melissa(operation_mode) + self.send({self._api.MODE: mode}) + + def turn_on(self): + """Turn on device.""" + self.send({self._api.STATE: self._api.STATE_ON}) + + def turn_off(self): + """Turn off device.""" + self.send({self._api.STATE: self._api.STATE_OFF}) + + def send(self, value): + """Sending action to service.""" + try: + old_value = self._cur_settings.copy() + self._cur_settings.update(value) + except AttributeError: + old_value = None + if not self._api.send(self._serial_number, self._cur_settings): + self._cur_settings = old_value + return False + else: + return True + + def update(self): + """Get latest data from Melissa.""" + try: + self._data = self._api.status(cached=True)[self._serial_number] + self._cur_settings = self._api.cur_settings( + self._serial_number + )['controller']['_relation']['command_log'] + except KeyError: + _LOGGER.warning( + 'Unable to update entity %s', self.entity_id) + + def melissa_state_to_hass(self, state): + """Translate Melissa states to hass states.""" + if state == self._api.STATE_ON: + return STATE_ON + elif state == self._api.STATE_OFF: + return STATE_OFF + elif state == self._api.STATE_IDLE: + return STATE_IDLE + else: + return None + + def melissa_op_to_hass(self, mode): + """Translate Melissa modes to hass states.""" + if mode == self._api.MODE_AUTO: + return STATE_AUTO + elif mode == self._api.MODE_HEAT: + return STATE_HEAT + elif mode == self._api.MODE_COOL: + return STATE_COOL + elif mode == self._api.MODE_DRY: + return STATE_DRY + elif mode == self._api.MODE_FAN: + return STATE_FAN_ONLY + else: + _LOGGER.warning( + "Operation mode %s could not be mapped to hass", mode) + return None + + def melissa_fan_to_hass(self, fan): + """Translate Melissa fan modes to hass modes.""" + if fan == self._api.FAN_AUTO: + return STATE_AUTO + elif fan == self._api.FAN_LOW: + return SPEED_LOW + elif fan == self._api.FAN_MEDIUM: + return SPEED_MEDIUM + elif fan == self._api.FAN_HIGH: + return SPEED_HIGH + else: + _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) + return None + + def hass_mode_to_melissa(self, mode): + """Translate hass states to melissa modes.""" + if mode == STATE_AUTO: + return self._api.MODE_AUTO + elif mode == STATE_HEAT: + return self._api.MODE_HEAT + elif mode == STATE_COOL: + return self._api.MODE_COOL + elif mode == STATE_DRY: + return self._api.MODE_DRY + elif mode == STATE_FAN_ONLY: + return self._api.MODE_FAN + else: + _LOGGER.warning("Melissa have no setting for %s mode", mode) + + def hass_fan_to_melissa(self, fan): + """Translate hass fan modes to melissa modes.""" + if fan == STATE_AUTO: + return self._api.FAN_AUTO + elif fan == SPEED_LOW: + return self._api.FAN_LOW + elif fan == SPEED_MEDIUM: + return self._api.FAN_MEDIUM + elif fan == SPEED_HIGH: + return self._api.FAN_HIGH + else: + _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 3656bf7b475..5929cec3b05 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -565,7 +565,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): @asyncio.coroutine def async_turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], self._payload_on, self._qos, self._retain) @@ -576,7 +576,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): @asyncio.coroutine def async_turn_aux_heat_off(self): - """Turn auxillary heater off.""" + """Turn auxiliary heater off.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], self._payload_off, self._qos, self._retain) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index ff1400a8fae..5553db70f0d 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -139,8 +139,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value) if self.gateway.optimistic: - # O - # ptimistically assume that device has changed state + # Optimistically assume that device has changed state self._values[value_type] = value self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index b4492821b1f..d8d7d6c901a 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS + @property + def unique_id(self): + """Unique ID for this device.""" + return self.device.serial + @property def name(self): """Return the name of the nest, if any.""" diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 7155aaf5924..5d54b39e773 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -24,7 +24,7 @@ CONF_RELAY = 'relay' CONF_THERMOSTAT = 'thermostat' DEFAULT_AWAY_TEMPERATURE = 14 -# # The default offeset is 2 hours (when you use the thermostat itself) +# # The default offset is 2 hours (when you use the thermostat itself) DEFAULT_TIME_OFFSET = 7200 # # Return cached results if last scan was less then this time ago # # NetAtmo Data is uploaded to server every hour diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py index 0cbdc8f2ce6..59f8db03318 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ThermostatDevice(ClimateDevice): - """Interface class for the oemthermostat modul.""" + """Interface class for the oemthermostat module.""" def __init__(self, hass, thermostat, name, away_temp): """Initialize the device.""" diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 25492cb0895..868511c0ac4 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -249,7 +249,7 @@ class TadoClimate(ClimateDevice): data = self._store.get_data(self._data_id) if data is None: - _LOGGER.debug("Recieved no data for zone %s", self.zone_name) + _LOGGER.debug("Received no data for zone %s", self.zone_name) return if 'sensorDataPoints' in data: @@ -294,7 +294,7 @@ class TadoClimate(ClimateDevice): overlay = False overlay_data = None - termination = self._current_operation + termination = CONST_MODE_SMART_SCHEDULE cooling = False fan_speed = CONST_MODE_OFF @@ -317,7 +317,7 @@ class TadoClimate(ClimateDevice): fan_speed = setting_data['fanSpeed'] if self._device_is_active: - # If you set mode manualy to off, there will be an overlay + # If you set mode manually to off, there will be an overlay # and a termination, but we want to see the mode "OFF" self._overlay_mode = termination self._current_operation = termination diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py index cc45e26a1cf..f9c5676629b 100644 --- a/homeassistant/components/climate/touchline.py +++ b/homeassistant/components/climate/touchline.py @@ -13,7 +13,7 @@ from homeassistant.components.climate import ( from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pytouchline==0.6'] +REQUIREMENTS = ['pytouchline==0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 92e5c71b6c5..6db1d53bc50 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -20,7 +20,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['venstarcolortouch==0.5'] +REQUIREMENTS = ['venstarcolortouch==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index a5bbf805d42..e17c9ee1b1e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -16,8 +16,7 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE) -from homeassistant.helpers import entityfilter -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import entityfilter, config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh @@ -105,12 +104,7 @@ def async_setup(hass, config): ) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) - - success = yield from cloud.initialize() - - if not success: - return False - + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) yield from http_api.async_setup(hass) return True @@ -192,19 +186,6 @@ class Cloud: return self._gactions_config - @asyncio.coroutine - def initialize(self): - """Initialize and load cloud info.""" - jwt_success = yield from self._fetch_jwt_keyset() - - if not jwt_success: - return False - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._start_cloud) - - return True - def path(self, *parts): """Get config path inside cloud dir. @@ -234,19 +215,34 @@ class Cloud: 'refresh_token': self.refresh_token, }, indent=4)) - def _start_cloud(self, event): + @asyncio.coroutine + def async_start(self, _): """Start the cloud component.""" - # Ensure config dir exists - path = self.hass.config.path(CONFIG_DIR) - if not os.path.isdir(path): - os.mkdir(path) + success = yield from self._fetch_jwt_keyset() - user_info = self.user_info_path - if not os.path.isfile(user_info): + # Fetching keyset can fail if internet is not up yet. + if not success: + self.hass.helpers.async_call_later(5, self.async_start) return - with open(user_info, 'rt') as file: - info = json.loads(file.read()) + def load_config(): + """Load config.""" + # Ensure config dir exists + path = self.hass.config.path(CONFIG_DIR) + if not os.path.isdir(path): + os.mkdir(path) + + user_info = self.user_info_path + if not os.path.isfile(user_info): + return None + + with open(user_info, 'rt') as file: + return json.loads(file.read()) + + info = yield from self.hass.async_add_job(load_config) + + if info is None: + return # Validate tokens try: diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py index bdb091325cf..10123752c99 100644 --- a/homeassistant/components/coinbase.py +++ b/homeassistant/components/coinbase.py @@ -5,16 +5,17 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/coinbase/ """ from datetime import timedelta - import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle + +REQUIREMENTS = ['coinbase==2.0.7'] -REQUIREMENTS = ['coinbase==2.0.6'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'coinbase' @@ -46,14 +47,13 @@ def setup(hass, config): api_secret = config[DOMAIN].get(CONF_API_SECRET) exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) - hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, - api_secret) + hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( + api_key, api_secret) if not hasattr(coinbase_data, 'accounts'): return False for account in coinbase_data.accounts.data: - load_platform(hass, 'sensor', DOMAIN, - {'account': account}, config) + load_platform(hass, 'sensor', DOMAIN, {'account': account}, config) for currency in exchange_currencies: if currency not in coinbase_data.exchange_rates.rates: _LOGGER.warning("Currency %s not found", currency) diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index 1fa215e5fb9..2736b656a15 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -68,7 +68,7 @@ class HMCover(HMDevice, CoverDevice): self._hmdevice.stop(self._channel) def _init_data_struct(self): - """Generate a data dictoinary (self._data) from metadata.""" + """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" self._data.update({self._state: STATE_UNKNOWN}) if "LEVEL_2" in self._hmdevice.WRITENODE: diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index b187b8409c2..7d77b1bc3be 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -42,7 +42,7 @@ def setup_platform(hass, config: ConfigType, class ISYCoverDevice(ISYDevice, CoverDevice): """Representation of an ISY994 cover device.""" - def __init__(self, node: object): + def __init__(self, node: object) -> None: """Initialize the ISY994 cover device.""" super().__init__(node) diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index 79c57c41e90..a6cd1263a73 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -74,7 +74,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up cover for KNX platform configured within plattform.""" + """Set up cover for KNX platform configured within platform.""" import xknx cover = xknx.devices.Cover( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py index 08a2ef8c5ad..4e38681a310 100644 --- a/homeassistant/components/cover/lutron.py +++ b/homeassistant/components/cover/lutron.py @@ -63,7 +63,7 @@ class LutronCover(LutronDevice, CoverDevice): def update(self): """Call when forcing a refresh of the device.""" - # Reading the property (rather than last_level()) fetchs value + # Reading the property (rather than last_level()) fetches value level = self._lutron_device.level _LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9b75f03c232..e55072dbc73 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -214,16 +214,6 @@ class MqttCover(MqttAvailability, CoverDevice): self.async_schedule_update_ha_state() - @callback - def availability_message_received(topic, payload, qos): - """Handle new MQTT availability messages.""" - if payload == self._payload_available: - self._available = True - elif payload == self._payload_not_available: - self._available = False - - self.async_schedule_update_ha_state() - if self._state_topic is None: # Force into optimistic mode. self._optimistic = True @@ -232,11 +222,6 @@ class MqttCover(MqttAvailability, CoverDevice): self.hass, self._state_topic, state_message_received, self._qos) - if self._availability_topic is not None: - yield from mqtt.async_subscribe( - self.hass, self._availability_topic, - availability_message_received, self._qos) - if self._tilt_status_topic is None: self._tilt_optimistic = True else: diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 1ee3ea00476..981312140eb 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice): rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) rpi_gpio.write_output(self._relay_pin, not self._invert_relay) - @property - def unique_id(self): - """Return the ID of this cover.""" - return '{}.{}'.format(self.__class__, self._name) - @property def name(self): """Return the name of the cover if any.""" diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 41be271fff0..1a3e020ed87 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -51,8 +51,8 @@ set_cover_tilt_position: entity_id: description: Name(s) of cover(s) to set cover tilt position. example: 'cover.living_room' - position: - description: Position of the cover (0 to 100). + tilt_position: + description: Tilt position of the cover (0 to 100). example: 30 stop_cover_tilt: diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index fd2b5847292..19bd9f01417 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.tahoma/ """ import logging -from datetime import timedelta from homeassistant.components.cover import CoverDevice from homeassistant.components.tahoma import ( @@ -15,8 +14,6 @@ DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Tahoma covers.""" diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index a7db472f191..f4728a12a3b 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -67,11 +67,6 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) -COVER_SCHEMA = vol.All( - cv.deprecated(CONF_ENTITY_ID), - COVER_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), }) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 3c038125616..15100957242 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -158,7 +158,7 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): @property def is_closing(self): - """Return true if cover is in an closing state.""" + """Return true if cover is in a closing state.""" return self._state == "Closing" @property diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 269b8136020..9d7d253c328 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -17,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==25'] +REQUIREMENTS = ['pydeconz==27'] _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ SERVICE_DATA = 'data' SERVICE_SCHEMA = vol.Schema({ vol.Required(SERVICE_FIELD): cv.string, - vol.Required(SERVICE_DATA): cv.string, + vol.Required(SERVICE_DATA): dict, }) CONFIG_INSTRUCTIONS = """ diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 64e1a60ad08..781e486a40e 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -42,7 +42,7 @@ Device = namedtuple('Device', ['mac', 'ip', 'last_update']) class ActiontecDeviceScanner(DeviceScanner): - """This class queries a an actiontec router for connected devices.""" + """This class queries an actiontec router for connected devices.""" def __init__(self, config): """Initialize the scanner.""" diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 2196dd78fdb..fb47b26a687 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -242,7 +242,7 @@ class _Connection: return self._connected def connect(self): - """Mark currenct connection state as connected.""" + """Mark current connection state as connected.""" self._connected = True def disconnect(self): diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py index b78683696cf..357dd0d36cf 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -2,7 +2,7 @@ Support for HUAWEI routers. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.huawei/ +https://home-assistant.io/components/device_tracker.huawei_router/ """ import base64 import logging @@ -119,7 +119,7 @@ class HuaweiDeviceScanner(DeviceScanner): cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host)) cnt_str = str(cnt.content, cnt.apparent_encoding, errors='replace') - _LOGGER.debug("Loggin in") + _LOGGER.debug("Logging in") cookie = requests.post('http://{}/login.cgi'.format(self.host), data=[('UserName', self.username), ('PassWord', self.password), diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py new file mode 100644 index 00000000000..0aa2be96290 --- /dev/null +++ b/homeassistant/components/device_tracker/mercedesme.py @@ -0,0 +1,71 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mercedesme/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.mercedesme import DATA_MME +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mercedesme'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Mercedes ME tracker.""" + if discovery_info is None: + return False + + data = hass.data[DATA_MME].data + + if not data.cars: + return False + + MercedesMEDeviceTracker(hass, config, see, data) + + return True + + +class MercedesMEDeviceTracker(object): + """A class representing a Mercedes ME device tracker.""" + + def __init__(self, hass, config, see, data): + """Initialize the Mercedes ME device tracker.""" + self.see = see + self.data = data + self.update_info() + + track_time_interval( + hass, self.update_info, MIN_TIME_BETWEEN_SCANS) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def update_info(self, now=None): + """Update the device info.""" + for device in self.data.cars: + _LOGGER.debug("Updating %s", device["vin"]) + location = self.data.get_location(device["vin"]) + if location is None: + return False + dev_id = device["vin"] + name = device["license"] + + lat = location['positionLat']['value'] + lon = location['positionLong']['value'] + attrs = { + 'trackr_id': dev_id, + 'id': dev_id, + 'name': name + } + self.see( + dev_id=dev_id, host_name=name, + gps=(lat, lon), attributes=attrs + ) + + return True diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 7ac84125863..1805559c252 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -REQUIREMENTS = ['librouteros==1.0.4'] +REQUIREMENTS = ['librouteros==1.0.5'] MTK_DEFAULT_API_PORT = '8728' diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index aab5b43acea..2e2d9b10d98 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - hass.async_add_job( - async_see(dev_id=dev_id_lookup[topic], location_name=payload)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + hass.async_add_job( + async_see(dev_id=dev_id, location_name=payload)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 0ef4f1835b6..7bcad60236a 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -41,32 +41,26 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - dev_id = dev_id_lookup[topic] - - try: - data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) - except vol.MultipleInvalid: - _LOGGER.error("Skipping update for following data " - "because of missing or malformatted data: %s", - payload) - return - except ValueError: - _LOGGER.error("Error parsing JSON payload: %s", payload) - return - - kwargs = _parse_see_args(dev_id, data) - hass.async_add_job( - async_see(**kwargs)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + try: + data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) + except vol.MultipleInvalid: + _LOGGER.error("Skipping update for following data " + "because of missing or malformatted data: %s", + payload) + return + except ValueError: + _LOGGER.error("Error parsing JSON payload: %s", payload) + return + + kwargs = _parse_see_args(dev_id, data) + hass.async_add_job(async_see(**kwargs)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 1742a0aed95..e99524c36db 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -143,6 +143,8 @@ def _parse_see_args(message, subscribe_topic): kwargs['attributes']['tid'] = message['tid'] if 'addr' in message: kwargs['attributes']['address'] = message['addr'] + if 'cog' in message: + kwargs['attributes']['course'] = message['cog'] if 't' in message: if message['t'] == 'c': kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 2306a66070b..e66bb95a11a 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -46,8 +46,8 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -def _refresh_on_acccess_denied(func): - """If remove rebooted, it lost our session so rebuld one and try again.""" +def _refresh_on_access_denied(func): + """If remove rebooted, it lost our session so rebuild one and try again.""" def decorator(self, *args, **kwargs): """Wrap the function to refresh session_id on PermissionError.""" try: @@ -95,16 +95,15 @@ class UbusDeviceScanner(DeviceScanner): """Must be implemented depending on the software.""" raise NotImplementedError - @_refresh_on_acccess_denied + @_refresh_on_access_denied def get_device_name(self, mac): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: self._generate_mac2name() name = self.mac2name.get(mac.upper(), None) - self.mac2name = None return name - @_refresh_on_acccess_denied + @_refresh_on_access_denied def _update_info(self): """Ensure the information from the router is up to date. @@ -122,13 +121,18 @@ class UbusDeviceScanner(DeviceScanner): self.last_results = [] results = 0 + # for each access point for hostapd in self.hostapd: result = _req_json_rpc( self.url, self.session_id, 'call', hostapd, 'get_clients') if result: results = results + 1 - self.last_results.extend(result['clients'].keys()) + # Check for each device is authorized (valid wpa key) + for key in result['clients'].keys(): + device = result['clients'][key] + if device['authorized']: + self.last_results.append(key) return bool(results) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index a3e81b3ef51..d5b6b044f1f 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -75,7 +75,7 @@ def get_scanner(hass, config): class UnifiScanner(DeviceScanner): """Provide device_tracker support from Unifi WAP client data.""" - def __init__(self, controller, detection_time: timedelta): + def __init__(self, controller, detection_time: timedelta) -> None: """Initialize the scanner.""" self._detection_time = detection_time self._controller = controller diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py index 0d6645f37c1..4bdb4c80add 100644 --- a/homeassistant/components/dominos.py +++ b/homeassistant/components/dominos.py @@ -1,7 +1,7 @@ """ Support for Dominos Pizza ordering. -The Dominos Pizza component ceates a service which can be invoked to order +The Dominos Pizza component creates a service which can be invoked to order from their menu For more details about this platform, please refer to the documentation at diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 56933d198f2..be7adc034a0 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -73,6 +73,7 @@ def setup(hass, config): class DoorbirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" + requires_auth = False url = API_URL name = API_URL[1:].replace('/', ':') extra_urls = [API_URL + '/{sensor}'] diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index d832bbdfdd1..b7354b4f0a7 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -79,7 +79,7 @@ def setup(hass, config): if req.status_code != 200: _LOGGER.warning( - "downloading '%s' failed, stauts_code=%d", + "downloading '%s' failed, status_code=%d", url, req.status_code) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 88cbf1bd57b..7ae4ec862bb 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -193,7 +193,7 @@ class EightSleepUserEntity(Entity): """The Eight Sleep device entity.""" def __init__(self, eight): - """Initialize the data oject.""" + """Initialize the data object.""" self._eight = eight @asyncio.coroutine @@ -217,7 +217,7 @@ class EightSleepHeatEntity(Entity): """The Eight Sleep device entity.""" def __init__(self, eight): - """Initialize the data oject.""" + """Initialize the data object.""" self._eight = eight @asyncio.coroutine diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py index 34d9fd0f458..6a92ab64044 100644 --- a/homeassistant/components/emoncms_history.py +++ b/homeassistant/components/emoncms_history.py @@ -59,7 +59,7 @@ def setup(hass, config): payload, fullurl, req.status_code) def update_emoncms(time): - """Send whitelisted entities states reguarly to Emoncms.""" + """Send whitelisted entities states regularly to Emoncms.""" payload_dict = {} for entity_id in whitelist: diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index b2206f80766..9fba21b81dc 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -39,6 +39,9 @@ CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_TYPE = 'type' +CONF_ENTITIES = 'entities' +CONF_ENTITY_NAME = 'name' +CONF_ENTITY_HIDDEN = 'hidden' TYPE_ALEXA = 'alexa' TYPE_GOOGLE = 'google_home' @@ -52,6 +55,11 @@ DEFAULT_EXPOSED_DOMAINS = [ ] DEFAULT_TYPE = TYPE_GOOGLE +CONFIG_ENTITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_ENTITY_NAME): cv.string, + vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_HOST_IP): cv.string, @@ -63,11 +71,14 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): - vol.Any(TYPE_ALEXA, TYPE_GOOGLE) + vol.Any(TYPE_ALEXA, TYPE_GOOGLE), + vol.Optional(CONF_ENTITIES): + vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA}) }) }, extra=vol.ALLOW_EXTRA) ATTR_EMULATED_HUE = 'emulated_hue' +ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' @@ -183,6 +194,8 @@ class Config(object): self.advertise_port = conf.get( CONF_ADVERTISE_PORT) or self.listen_port + self.entities = conf.get(CONF_ENTITIES, {}) + def entity_id_to_number(self, entity_id): """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: @@ -215,6 +228,14 @@ class Config(object): assert isinstance(number, str) return self.numbers.get(number) + def get_entity_name(self, entity): + """Get the name of an entity.""" + if entity.entity_id in self.entities and \ + CONF_ENTITY_NAME in self.entities[entity.entity_id]: + return self.entities[entity.entity_id][CONF_ENTITY_NAME] + + return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) + def is_entity_exposed(self, entity): """Determine if an entity should be exposed on the emulated bridge. @@ -227,6 +248,12 @@ class Config(object): domain = entity.domain.lower() explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None) + + if entity.entity_id in self.entities and \ + CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]: + explicit_hidden = \ + self.entities[entity.entity_id][CONF_ENTITY_HIDDEN] + if explicit_expose is True or explicit_hidden is False: expose = True elif explicit_expose is False or explicit_hidden is True: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 7b98ca7deaa..5d97ef3cea4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -24,9 +24,6 @@ from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) -ATTR_EMULATED_HUE = 'emulated_hue' -ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' - HUE_API_STATE_ON = 'on' HUE_API_STATE_BRI = 'bri' @@ -77,7 +74,7 @@ class HueAllLightsStateView(HomeAssistantView): number = self.config.entity_id_to_number(entity.entity_id) json_response[number] = entity_to_json( - entity, state, brightness) + self.config, entity, state, brightness) return self.json(json_response) @@ -110,7 +107,7 @@ class HueOneLightStateView(HomeAssistantView): state, brightness = get_entity_state(self.config, entity) - json_response = entity_to_json(entity, state, brightness) + json_response = entity_to_json(self.config, entity, state, brightness) return self.json(json_response) @@ -344,10 +341,8 @@ def get_entity_state(config, entity): return (final_state, final_brightness) -def entity_to_json(entity, is_on=None, brightness=None): +def entity_to_json(config, entity, is_on=None, brightness=None): """Convert an entity to its Hue bridge JSON representation.""" - name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) - return { 'state': { @@ -356,7 +351,7 @@ def entity_to_json(entity, is_on=None, brightness=None): 'reachable': True }, 'type': 'Dimmable light', - 'name': name, + 'name': config.get_entity_name(entity), 'modelid': 'HASS123', 'uniqueid': entity.entity_id, 'swversion': '123' diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index eccc800319c..6e6d377986d 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -205,7 +205,7 @@ def async_setup(hass, config: dict): @asyncio.coroutine def async_handle_fan_service(service): - """Hande service call for fans.""" + """Handle service call for fans.""" method = SERVICE_TO_METHOD.get(service.service) params = service.data.copy() diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py index ab32e588c03..c6d1232801f 100644 --- a/homeassistant/components/fan/comfoconnect.py +++ b/homeassistant/components/fan/comfoconnect.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge): + def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" from pycomfoconnect import SENSOR_FAN_SPEED_MODE @@ -93,7 +93,7 @@ class ComfoConnectFan(FanEntity): speed = SPEED_LOW self.set_speed(speed) - def turn_off(self) -> None: + def turn_off(self, **kwargs) -> None: """Turn off the fan (to away).""" self.set_speed(SPEED_OFF) diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index 85e603c8c81..e6f9424d852 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}_fan'.format(self.node.device_id) + return self.node.device_id @property def speed(self) -> str: diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 910e33627a6..942aff4ec57 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.4'] +REQUIREMENTS = ['python-miio==0.3.5'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' @@ -200,7 +200,7 @@ class XiaomiAirPurifier(FanEntity): @asyncio.coroutine def _try_command(self, mask_error, func, *args, **kwargs): - """Call a air purifier command handling error messages.""" + """Call an air purifier command handling error messages.""" from miio import DeviceException try: result = yield from self.hass.async_add_job( diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 3d73901b4d8..2c0e146491a 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -153,8 +153,7 @@ class StoredData(object): with self._lock, open(self._data_file, 'rb') as myfile: self._data = pickle.load(myfile) or {} self._cache_outdated = False - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error loading data from pickled file %s", self._data_file) @@ -172,8 +171,7 @@ class StoredData(object): url, self._data_file) try: pickle.dump(self._data, myfile) - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error( "Error saving pickled data to %s", self._data_file) self._cache_outdated = True diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index a601dcbdc51..eedd33478a7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180130.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180209.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -408,7 +408,7 @@ def async_setup_themes(hass, themes): @callback def set_theme(call): - """Set backend-prefered theme.""" + """Set backend-preferred theme.""" data = call.data name = data[CONF_NAME] if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]: @@ -585,11 +585,13 @@ def _is_latest(js_option, request): return useragent.os.version[0] >= 12 family_min_version = { - 'Chrome': 50, # Probably can reduce this - 'Firefox': 43, # Array.protopype.includes added in 43 - 'Opera': 40, # Probably can reduce this - 'Edge': 14, # Array.protopype.includes added in 14 - 'Safari': 10, # many features not supported by 9 + 'Chrome': 54, # Object.values + 'Chrome Mobile': 54, + 'Firefox': 47, # Object.values + 'Firefox Mobile': 47, + 'Opera': 41, # Object.values + 'Edge': 14, # Array.prototype.includes added in 14 + 'Safari': 10, # Many features not supported by 9 } version = family_min_version.get(useragent.browser.family) return version and useragent.browser.version[0] >= version diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py new file mode 100644 index 00000000000..f360d4ffba9 --- /dev/null +++ b/homeassistant/components/goalfeed.py @@ -0,0 +1,62 @@ +""" +Component for the Goalfeed service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/goalfeed/ +""" +import json + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +REQUIREMENTS = ['pysher==0.2.0'] + +DOMAIN = 'goalfeed' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +GOALFEED_HOST = 'feed.goalfeed.ca' +GOALFEED_AUTH_ENDPOINT = 'https://goalfeed.ca/feed/auth' +GOALFEED_APP_ID = 'bfd4ed98c1ff22c04074' + + +def setup(hass, config): + """Set up the Goalfeed component.""" + import pysher + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + def goal_handler(data): + """Handle goal events.""" + goal = json.loads(json.loads(data)) + + hass.bus.fire('goal', event_data=goal) + + def connect_handler(data): + """Handle connection.""" + post_data = { + 'username': username, + 'password': password, + 'connection_info': data} + resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, + timeout=30).json() + + channel = pusher.subscribe('private-goals', resp['auth']) + channel.bind('goal', goal_handler) + + pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080, + custom_host=GOALFEED_HOST) + + pusher.connection.bind('pusher:connection_established', connect_handler) + pusher.connect() + + return True diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index f7923067270..30151ee1a56 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -128,7 +128,7 @@ def do_authentication(hass, config): """Keep trying to validate the user_code until it expires.""" if now >= dt.as_local(dev_flow.user_code_expiry): hass.components.persistent_notification.create( - 'Authenication code expired, please restart ' + 'Authentication code expired, please restart ' 'Home-Assistant and try again', title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index d8e9f668c8e..b718c009160 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -37,6 +37,7 @@ from .const import ( ) HANDLERS = Registry() +QUERY_HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) # Mapping is [actions schema, primary trait, optional features] @@ -79,7 +80,7 @@ class SmartHomeError(Exception): """Log error code.""" super(SmartHomeError, self).__init__(msg) _LOGGER.error( - "An error has ocurred in Google SmartHome: %s." + "An error has occurred in Google SmartHome: %s." "Error code: %s", msg, code ) self.code = code @@ -96,7 +97,7 @@ class Config: def entity_to_device(entity: Entity, config: Config, units: UnitSystem): - """Convert a hass entity into an google actions device.""" + """Convert a hass entity into a google actions device.""" entity_config = config.entity_config.get(entity.entity_id, {}) google_domain = entity_config.get(CONF_TYPE) class_data = MAPPING_COMPONENT.get( @@ -177,120 +178,145 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): return device -def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: - """Take an entity and return a properly formatted device object.""" - def celsius(deg: Optional[float]) -> Optional[float]: - """Convert a float to Celsius and rounds to one decimal place.""" - if deg is None: - return None - return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) +def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]: + """Convert a float to Celsius and rounds to one decimal place.""" + if deg is None: + return None + return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) - if entity.domain == sensor.DOMAIN: - entity_config = config.entity_config.get(entity.entity_id, {}) - google_domain = entity_config.get(CONF_TYPE) - if google_domain == climate.DOMAIN: - # check if we have a string value to convert it to number - value = entity.state - if isinstance(entity.state, str): - try: - value = float(value) - except ValueError: - value = None - - if value is None: - raise SmartHomeError( - ERROR_NOT_SUPPORTED, - "Invalid value {} for the climate sensor" - .format(entity.state) - ) - - # detect if we report temperature or humidity - unit_of_measurement = entity.attributes.get( - ATTR_UNIT_OF_MEASUREMENT, - units.temperature_unit - ) - if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: - value = celsius(value) - attr = 'thermostatTemperatureAmbient' - elif unit_of_measurement == '%': - attr = 'thermostatHumidityAmbient' - else: - raise SmartHomeError( - ERROR_NOT_SUPPORTED, - "Unit {} is not supported by the climate sensor" - .format(unit_of_measurement) - ) - - return {attr: value} +@QUERY_HANDLERS.register(sensor.DOMAIN) +def query_response_sensor( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a sensor entity to a QUERY response.""" + entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) + if google_domain != climate.DOMAIN: raise SmartHomeError( ERROR_NOT_SUPPORTED, "Sensor type {} is not supported".format(google_domain) ) - if entity.domain == climate.DOMAIN: - mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() - if mode not in CLIMATE_SUPPORTED_MODES: - mode = 'heat' - response = { - 'thermostatMode': mode, - 'thermostatTemperatureSetpoint': - celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)), - 'thermostatTemperatureAmbient': - celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)), - 'thermostatTemperatureSetpointHigh': - celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)), - 'thermostatTemperatureSetpointLow': - celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)), - 'thermostatHumidityAmbient': - entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY), - } - return {k: v for k, v in response.items() if v is not None} + # check if we have a string value to convert it to number + value = entity.state + if isinstance(entity.state, str): + try: + value = float(value) + except ValueError: + value = None - final_state = entity.state != STATE_OFF - final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255 - if final_state else 0) + if value is None: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Invalid value {} for the climate sensor" + .format(entity.state) + ) - if entity.domain == media_player.DOMAIN: - level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0 - if final_state else 0.0) - # Convert 0.0-1.0 to 0-255 - final_brightness = round(min(1.0, level) * 255) + # detect if we report temperature or humidity + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: + value = celsius(value, units) + attr = 'thermostatTemperatureAmbient' + elif unit_of_measurement == '%': + attr = 'thermostatHumidityAmbient' + else: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Unit {} is not supported by the climate sensor" + .format(unit_of_measurement) + ) - if final_brightness is None: - final_brightness = 255 if final_state else 0 + return {attr: value} - final_brightness = 100 * (final_brightness / 255) - query_response = { - "on": final_state, - "online": True, - "brightness": int(final_brightness) +@QUERY_HANDLERS.register(climate.DOMAIN) +def query_response_climate( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a climate entity to a QUERY response.""" + mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() + if mode not in CLIMATE_SUPPORTED_MODES: + mode = 'heat' + attrs = entity.attributes + response = { + 'thermostatMode': mode, + 'thermostatTemperatureSetpoint': + celsius(attrs.get(climate.ATTR_TEMPERATURE), units), + 'thermostatTemperatureAmbient': + celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units), + 'thermostatTemperatureSetpointHigh': + celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units), + 'thermostatTemperatureSetpointLow': + celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units), + 'thermostatHumidityAmbient': + attrs.get(climate.ATTR_CURRENT_HUMIDITY), } + return {k: v for k, v in response.items() if v is not None} + + +@QUERY_HANDLERS.register(media_player.DOMAIN) +def query_response_media_player( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a media_player entity to a QUERY response.""" + level = entity.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL, + 1.0 if entity.state != STATE_OFF else 0.0) + # Convert 0.0-1.0 to 0-255 + brightness = int(level * 100) + + return {'brightness': brightness} + + +@QUERY_HANDLERS.register(light.DOMAIN) +def query_response_light( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a light entity to a QUERY response.""" + response = {} # type: Dict[str, Any] + + brightness = entity.attributes.get(light.ATTR_BRIGHTNESS) + if brightness is not None: + response['brightness'] = int(100 * (brightness / 255)) supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported_features & \ (light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR): - query_response["color"] = {} + response['color'] = {} if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None: - query_response["color"]["temperature"] = \ + response['color']['temperature'] = \ int(round(color.color_temperature_mired_to_kelvin( entity.attributes.get(light.ATTR_COLOR_TEMP)))) if entity.attributes.get(light.ATTR_COLOR_NAME) is not None: - query_response["color"]["name"] = \ + response['color']['name'] = \ entity.attributes.get(light.ATTR_COLOR_NAME) if entity.attributes.get(light.ATTR_RGB_COLOR) is not None: color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR) if color_rgb is not None: - query_response["color"]["spectrumRGB"] = \ + response['color']['spectrumRGB'] = \ int(color.color_rgb_to_hex( color_rgb[0], color_rgb[1], color_rgb[2]), 16) - return query_response + return response + + +def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: + """Take an entity and return a properly formatted device object.""" + state = entity.state != STATE_OFF + defaults = { + 'on': state, + 'online': True + } + + handler = QUERY_HANDLERS.get(entity.domain) + if callable(handler): + defaults.update(handler(entity, config, units)) + + return defaults # erroneous bug on old pythons and pylint @@ -429,7 +455,7 @@ def async_devices_query(hass, config, payload): devices = {} for device in payload.get('devices', []): devid = device.get('id') - # In theory this should never happpen + # In theory this should never happen if not devid: _LOGGER.error('Device missing ID: %s', device) continue @@ -438,11 +464,11 @@ def async_devices_query(hass, config, payload): if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} - - try: - devices[devid] = query_device(state, config, hass.config.units) - except SmartHomeError as error: - devices[devid] = {'errorCode': error.code} + else: + try: + devices[devid] = query_device(state, config, hass.config.units) + except SmartHomeError as error: + devices[devid] = {'errorCode': error.code} return {'devices': devices} diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index a8529f18b69..5e4dfdb0bdc 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -247,7 +247,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): @asyncio.coroutine def async_setup(hass, config): - """Set up all groups found definded in the configuration.""" + """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) if component is None: @@ -371,7 +371,6 @@ def async_setup(hass, config): @asyncio.coroutine def _async_process_config(hass, config, component): """Process group configuration.""" - groups = [] for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) entity_ids = conf.get(CONF_ENTITIES) or [] @@ -381,13 +380,9 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - group = yield from Group.async_create_group( + yield from Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) - groups.append(group) - - if groups: - yield from component.async_add_entities(groups) class Group(Entity): diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index f94dd8816a7..8e2464d0922 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -320,7 +320,7 @@ def setup(hass: HomeAssistant, base_config): class CecDevice(Entity): """Representation of a HDMI CEC device entity.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the device.""" self._device = device self.hass = hass diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index db2a43d8728..9c08984a23e 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.38'] +REQUIREMENTS = ['pyhomematic==0.1.39'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) @@ -218,7 +218,7 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({ @bind_hass def virtualkey(hass, address, channel, param, interface=None): - """Send virtual keypress to homematic controlller.""" + """Send virtual keypress to homematic controller.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, @@ -256,7 +256,7 @@ def set_device_value(hass, address, channel, param, value, interface=None): @bind_hass def set_install_mode(hass, interface, mode=None, time=None, address=None): - """Call setInstallMode XML-RPC method of supplied inteface.""" + """Call setInstallMode XML-RPC method of supplied interface.""" data = { key: value for key, value in ( (ATTR_INTERFACE, interface), @@ -466,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args): hass, discovery_type, addresses, interface) # When devices of this type are found - # they are setup in HASS and an discovery event is fired + # they are setup in HASS and a discovery event is fired if found_devices: discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices @@ -665,7 +665,7 @@ class HMHub(Entity): self.schedule_update_ha_state() def _update_variables(self, now): - """Retrive all variable data and update hmvariable states.""" + """Retrieve all variable data and update hmvariable states.""" variables = self._homematic.getAllSystemVariables(self._name) if variables is None: return diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index bf4d99af9e7..c2946b51842 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -13,7 +13,7 @@ virtualkey: description: Event to send i.e. PRESS_LONG, PRESS_SHORT. example: PRESS_LONG interface: - description: (Optional) for set a interface value. + description: (Optional) for set an interface value. example: Interfaces name from config set_variable_value: @@ -42,7 +42,7 @@ set_device_value: description: Event to send i.e. PRESS_LONG, PRESS_SHORT example: PRESS_LONG interface: - description: (Optional) for set a interface value + description: (Optional) for set an interface value example: Interfaces name from config value: description: New value diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index ce5bfca3ac1..a6a412b6ba2 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -23,7 +23,7 @@ def auth_middleware(request, handler): # If no password set, just always set authenticated=True if request.app['hass'].http.api_password is None: request[KEY_AUTHENTICATED] = True - return handler(request) + return (yield from handler(request)) # Check authentication authenticated = False @@ -46,7 +46,7 @@ def auth_middleware(request, handler): authenticated = True request[KEY_AUTHENTICATED] = authenticated - return handler(request) + return (yield from handler(request)) def is_trusted_ip(request): diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c9b094e3f2e..b34df1897f0 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -6,7 +6,7 @@ from aiohttp import hdrs from aiohttp.web import FileResponse, middleware from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_urldispatcher import StaticResource -from yarl import unquote +from yarl import URL _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) @@ -16,7 +16,7 @@ class CachingStaticResource(StaticResource): @asyncio.coroutine def _handle(self, request): - filename = unquote(request.match_info['filename']) + filename = URL(request.match_info['filename']).path try: # PyLint is wrong about resolve not being a member. # pylint: disable=no-member @@ -67,7 +67,7 @@ def staticresource_middleware(request, handler): """Middleware to strip out fingerprint from fingerprinted assets.""" path = request.path if not path.startswith('/static/') and not path.startswith('/frontend'): - return handler(request) + return (yield from handler(request)) fingerprinted = _FINGERPRINT.match(request.match_info['filename']) @@ -75,4 +75,4 @@ def staticresource_middleware(request, handler): request.match_info['filename'] = \ '{}.{}'.format(*fingerprinted.groups()) - return handler(request) + return (yield from handler(request)) diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 48827851f92..999dda42015 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -14,7 +14,7 @@ class IHCDevice(Entity): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - product: Element=None): + product: Element=None) -> None: """Initialize IHC attributes.""" self.ihc_controller = ihc_controller self._name = name diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 646bfcf421f..2c2b8364823 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -60,7 +60,7 @@ SERVICE_SCAN_SCHEMA = vol.Schema({ @bind_hass def scan(hass, entity_id=None): - """Force process a image.""" + """Force process an image.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.services.call(DOMAIN, SERVICE_SCAN, data) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 40aac61914b..6770ff1bdf6 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -36,7 +36,7 @@ def validate_attributes(list_attributes): """Validate face attributes.""" for attr in list_attributes: if attr not in SUPPORTED_ATTRIBUTES: - raise vol.Invalid("Invalid attribtue {0}".format(attr)) + raise vol.Invalid("Invalid attribute {0}".format(attr)) return list_attributes diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 0cdd1675274..258731326ee 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -201,7 +201,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): return # Parse data - knwon_faces = [] + known_faces = [] total = 0 for face in detect: total += 1 @@ -215,9 +215,9 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): name = s_name break - knwon_faces.append({ + known_faces.append({ ATTR_NAME: name, ATTR_CONFIDENCE: data['confidence'] * 100, }) - self.async_process_faces(knwon_faces, total) + self.async_process_faces(known_faces, total) diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 2fdc3d72f2e..dbf36dcd86e 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -109,12 +109,14 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): websession = async_get_clientsession(self.hass) params = self._params.copy() - params['image_bytes'] = str(b64encode(image), 'utf-8') + body = { + 'image_bytes': str(b64encode(image), 'utf-8') + } try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): request = yield from websession.post( - OPENALPR_API_URL, params=params + OPENALPR_API_URL, params=params, data=body ) data = yield from request.json() diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py index 60b2eadee92..1ef8a4bb847 100644 --- a/homeassistant/components/image_processing/seven_segments.py +++ b/homeassistant/components/image_processing/seven_segments.py @@ -66,7 +66,7 @@ class ImageProcessingSsocr(ImageProcessingEntity): if name: self._name = name else: - self._name = "SevenSegement OCR {0}".format( + self._name = "SevenSegment OCR {0}".format( split_entity_id(camera_entity)[1]) self._state = None diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 82f98449411..526b8057ce1 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -4,10 +4,11 @@ A component which allows you to send data to an Influx database. For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ -from datetime import timedelta -from functools import partial, wraps import logging import re +import queue +import threading +import time import requests.exceptions import voluptuous as vol @@ -15,13 +16,13 @@ import voluptuous as vol from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) + EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, + STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues -from homeassistant.util import utcnow -REQUIREMENTS = ['influxdb==4.1.1'] +REQUIREMENTS = ['influxdb==5.0.0'] _LOGGER = logging.getLogger(__name__) @@ -39,14 +40,17 @@ CONF_RETRY_QUEUE = 'retry_queue_limit' DEFAULT_DATABASE = 'home_assistant' DEFAULT_VERIFY_SSL = True DOMAIN = 'influxdb' + TIMEOUT = 5 +RETRY_DELAY = 20 +QUEUE_BACKLOG_SECONDS = 10 COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All(cv.deprecated(CONF_RETRY_QUEUE), vol.Schema({ vol.Optional(CONF_HOST): cv.string, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, @@ -78,7 +82,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), - }), + })), }, extra=vol.ALLOW_EXTRA) RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$') @@ -127,7 +131,6 @@ def setup(hass, config): conf[CONF_COMPONENT_CONFIG_DOMAIN], conf[CONF_COMPONENT_CONFIG_GLOB]) max_tries = conf.get(CONF_RETRY_COUNT) - queue_limit = conf.get(CONF_RETRY_QUEUE) try: influx = InfluxDBClient(**kwargs) @@ -137,22 +140,21 @@ def setup(hass, config): _LOGGER.error("Database host is not accessible due to '%s', please " "check your entries in the configuration file (host, " "port, etc.) and verify that the database exists and is " - "READ/WRITE.", exc) + "READ/WRITE", exc) return False - def influx_event_listener(event): - """Listen for new messages on the bus and sends them to Influx.""" + def influx_handle_event(event): + """Send an event to Influx.""" state = event.data.get('new_state') if state is None or state.state in ( STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ - state.entity_id in blacklist_e or \ - state.domain in blacklist_d: - return + state.entity_id in blacklist_e or state.domain in blacklist_d: + return True try: if (whitelist_e and state.entity_id not in whitelist_e) or \ (whitelist_d and state.domain not in whitelist_d): - return + return True _include_state = _include_value = False @@ -222,93 +224,78 @@ def setup(hass, config): json_body[0]['tags'].update(tags) - _write_data(json_body) - - @RetryOnError(hass, retry_limit=max_tries, retry_delay=20, - queue_limit=queue_limit) - def _write_data(json_body): - """Write the data.""" try: influx.write_points(json_body) - except exceptions.InfluxDBClientError: - _LOGGER.exception("Error saving event %s to InfluxDB", json_body) + return True + except (exceptions.InfluxDBClientError, IOError): + return False - hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) + instance = hass.data[DOMAIN] = InfluxThread( + hass, influx_handle_event, max_tries) + instance.start() + + def shutdown(event): + """Shut down the thread.""" + instance.queue.put(None) + instance.join() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) return True -class RetryOnError(object): - """A class for retrying a failed task a certain amount of tries. +class InfluxThread(threading.Thread): + """A threaded event handler class.""" - This method decorator makes a method retrying on errors. If there was an - uncaught exception, it schedules another try to execute the task after a - retry delay. It does this up to the maximum number of retries. + def __init__(self, hass, event_handler, max_tries): + """Initialize the listener.""" + threading.Thread.__init__(self, name='InfluxDB') + self.queue = queue.Queue() + self.event_handler = event_handler + self.max_tries = max_tries + hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) - It can be used for all probable "self-healing" problems like network - outages. The task will be rescheduled using HAs scheduling mechanism. + def _event_listener(self, event): + """Listen for new messages on the bus and queue them for Influx.""" + item = (time.monotonic(), event) + self.queue.put(item) - It takes a Hass instance, a maximum number of retries and a retry delay - in seconds as arguments. + def run(self): + """Process incoming events.""" + queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY - The queue limit defines the maximum number of calls that are allowed to - be queued at a time. If this number is reached, every new call discards - an old one. - """ + write_error = False + dropped = False - def __init__(self, hass, retry_limit=0, retry_delay=20, queue_limit=100): - """Initialize the decorator.""" - self.hass = hass - self.retry_limit = retry_limit - self.retry_delay = timedelta(seconds=retry_delay) - self.queue_limit = queue_limit + while True: + item = self.queue.get() - def __call__(self, method): - """Decorate the target method.""" - from homeassistant.helpers.event import track_point_in_utc_time + if item is None: + self.queue.task_done() + return - @wraps(method) - def wrapper(*args, **kwargs): - """Wrap method.""" - # pylint: disable=protected-access - if not hasattr(wrapper, "_retry_queue"): - wrapper._retry_queue = [] + timestamp, event = item + age = time.monotonic() - timestamp - def scheduled(retry=0, untrack=None, event=None): - """Call the target method. + if age < queue_seconds: + for retry in range(self.max_tries+1): + if self.event_handler(event): + if write_error: + _LOGGER.error("Resumed writing to InfluxDB") + write_error = False + dropped = False + break + elif retry < self.max_tries: + time.sleep(RETRY_DELAY) + elif not write_error: + _LOGGER.error("Error writing to InfluxDB") + write_error = True + elif not dropped: + _LOGGER.warning("Dropping old events to catch up") + dropped = True - It is called directly at the first time and then called - scheduled within the Hass mainloop. - """ - if untrack is not None: - wrapper._retry_queue.remove(untrack) + self.queue.task_done() - # pylint: disable=broad-except - try: - method(*args, **kwargs) - except Exception as ex: - if retry == self.retry_limit: - raise - if len(wrapper._retry_queue) >= self.queue_limit: - last = wrapper._retry_queue.pop(0) - if 'remove' in last: - func = last['remove'] - func() - if 'exc' in last: - _LOGGER.error( - "Retry queue overflow, drop oldest entry: %s", - str(last['exc'])) - - target = utcnow() + self.retry_delay - tracking = {'target': target} - remove = track_point_in_utc_time(self.hass, - partial(scheduled, - retry + 1, - tracking), - target) - tracking['remove'] = remove - tracking["exc"] = ex - wrapper._retry_queue.append(tracking) - - scheduled() - return wrapper + def block_till_done(self): + """Block till all events processed.""" + self.queue.join() diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 43feeb8c4f4..56761b5af4e 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -137,7 +137,7 @@ class InputBoolean(ToggleEntity): @property def icon(self): - """Returh the icon to be used for this entity.""" + """Return the icon to be used for this entity.""" return self._icon @property diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 583181fe453..6433a01fb6d 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -26,10 +26,14 @@ CONF_INITIAL = 'initial' CONF_MIN = 'min' CONF_MAX = 'max' +MODE_TEXT = 'text' +MODE_PASSWORD = 'password' + ATTR_VALUE = 'value' ATTR_MIN = 'min' ATTR_MAX = 'max' ATTR_PATTERN = 'pattern' +ATTR_MODE = 'mode' SERVICE_SET_VALUE = 'set_value' @@ -63,6 +67,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_ICON): cv.icon, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): + vol.In([MODE_TEXT, MODE_PASSWORD]), }, _cv_input_text) }) }, required=True, extra=vol.ALLOW_EXTRA) @@ -92,10 +98,11 @@ def async_setup(hass, config): icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) pattern = cfg.get(ATTR_PATTERN) + mode = cfg.get(CONF_MODE) entities.append(InputText( object_id, name, initial, minimum, maximum, icon, unit, - pattern)) + pattern, mode)) if not entities: return False @@ -122,7 +129,7 @@ class InputText(Entity): """Represent a text box.""" def __init__(self, object_id, name, initial, minimum, maximum, icon, - unit, pattern): + unit, pattern, mode): """Initialize a text input.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name @@ -132,6 +139,7 @@ class InputText(Entity): self._icon = icon self._unit = unit self._pattern = pattern + self._mode = mode @property def should_poll(self): @@ -165,6 +173,7 @@ class InputText(Entity): ATTR_MIN: self._minimum, ATTR_MAX: self._maximum, ATTR_PATTERN: self._pattern, + ATTR_MODE: self._mode, } @asyncio.coroutine diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index ebabcdb0e79..fe3c934659b 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -72,7 +72,7 @@ ATTR_DEVICE_SYSTEM_VERSION = 'systemVersion' ATTR_DEVICE_TYPE = 'type' ATTR_DEVICE_SYSTEM_NAME = 'systemName' -ATTR_APP_BUNDLE_IDENTIFER = 'bundleIdentifer' +ATTR_APP_BUNDLE_IDENTIFIER = 'bundleIdentifier' ATTR_APP_BUILD_NUMBER = 'buildNumber' ATTR_APP_VERSION_NUMBER = 'versionNumber' @@ -136,7 +136,7 @@ IDENTIFY_DEVICE_SCHEMA = vol.Schema({ IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA) IDENTIFY_APP_SCHEMA = vol.Schema({ - vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string, + vol.Required(ATTR_APP_BUNDLE_IDENTIFIER): cv.string, vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int, vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string }, extra=vol.ALLOW_EXTRA) @@ -182,7 +182,7 @@ def enabled_push_ids(): """Return a list of push enabled target push IDs.""" push_ids = list() # pylint: disable=unused-variable - for device_name, device in CONFIG_FILE[ATTR_DEVICES].items(): + for device in CONFIG_FILE[ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) return push_ids diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py index 237493c7919..442be6e22e7 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyota==2.0.3'] +REQUIREMENTS = ['pyota==2.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 28cfac39154..d85883e472a 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -123,7 +123,7 @@ SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', 'light', 'switch'] SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] -# ISY Scenes are more like Swithes than Hass Scenes +# ISY Scenes are more like Switches than Hass Scenes # (they can turn off, and report their state) SCENE_DOMAIN = 'switch' diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b761b04c705..cfeceb0c991 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -254,7 +254,7 @@ def async_setup(hass, config): @asyncio.coroutine def async_handle_light_service(service): - """Hande a turn light on or off service call.""" + """Handle a turn light on or off service call.""" # Get the validated data params = service.data.copy() diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index f214d47fa1b..5344c3dce6d 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -83,7 +83,7 @@ class AvionLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 6b22190dce9..529917c36e2 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -9,7 +9,7 @@ import asyncio from homeassistant.components.deconz import DOMAIN as DECONZ_DATA from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + 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) from homeassistant.core import callback @@ -100,6 +100,11 @@ class DeconzLight(Light): """Return the name of the light.""" return self._light.name + @property + def unique_id(self): + """Return a unique identifier for this light.""" + return self._light.uniqueid + @property def supported_features(self): """Flag supported features.""" @@ -129,6 +134,9 @@ class DeconzLight(Light): data['xy'] = xyb[0], xyb[1] data['bri'] = xyb[2] + if ATTR_XY_COLOR in kwargs: + data['xy'] = kwargs[ATTR_XY_COLOR] + if ATTR_BRIGHTNESS in kwargs: data['bri'] = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index 6d502e15d6f..03441dd8ea6 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def retry(method): """Retry bluetooth commands.""" @wraps(method) - def wrapper_retry(device, *args, **kwds): + def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" # pylint: disable=import-error import decora @@ -46,7 +46,7 @@ def retry(method): if time.monotonic() - initial >= 10: return None try: - return method(device, *args, **kwds) + return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): _LOGGER.warning("Decora connect error for device %s. " @@ -88,7 +88,7 @@ class DecoraLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index c48de4deaf8..075b98117f8 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -46,7 +46,7 @@ EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade' EFFECT_COLORSTROBE = 'colorstrobe' EFFECT_RED_STROBE = 'red_strobe' EFFECT_GREEN_STROBE = 'green_strobe' -EFFECT_BLUE_STOBE = 'blue_strobe' +EFFECT_BLUE_STROBE = 'blue_strobe' EFFECT_YELLOW_STROBE = 'yellow_strobe' EFFECT_CYAN_STROBE = 'cyan_strobe' EFFECT_PURPLE_STROBE = 'purple_strobe' @@ -68,7 +68,7 @@ EFFECT_MAP = { EFFECT_COLORSTROBE: 0x30, EFFECT_RED_STROBE: 0x31, EFFECT_GREEN_STROBE: 0x32, - EFFECT_BLUE_STOBE: 0x33, + EFFECT_BLUE_STROBE: 0x33, EFFECT_YELLOW_STROBE: 0x34, EFFECT_CYAN_STROBE: 0x35, EFFECT_PURPLE_STROBE: 0x36, @@ -167,11 +167,6 @@ class FluxLight(Light): """Return True if entity is available.""" return self._bulb is not None - @property - def unique_id(self): - """Return the ID of this light.""" - return '{}.{}'.format(self.__class__, self._ipaddr) - @property def name(self): """Return the name of the device if any.""" diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py index 5ad7fd4c317..8e9d93657ce 100644 --- a/homeassistant/components/light/greenwave.py +++ b/homeassistant/components/light/greenwave.py @@ -38,18 +38,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): tokenfile = hass.config.path('.greenwave') if config.get(CONF_VERSION) == 3: if os.path.exists(tokenfile): - tokenfile = open(tokenfile) - token = tokenfile.read() - tokenfile.close() + with open(tokenfile) as tokenfile: + token = tokenfile.read() else: try: token = greenwave.grab_token(host, 'hass', 'homeassistant') except PermissionError: _LOGGER.error('The Gateway Is Not In Sync Mode') raise - tokenfile = open(tokenfile, "w+") - tokenfile.write(token) - tokenfile.close() + with open(tokenfile, "w+") as tokenfile: + tokenfile.write(token) else: token = None bulbs = greenwave.grab_bulbs(host, token) diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index 8fafb88a7db..5ba162a20d2 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -137,5 +137,5 @@ class HiveDeviceLight(Light): return supported_features def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 807c19fffdb..a3db1ff30ff 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -1,5 +1,5 @@ """ -Support for Homematic lighs. +Support for Homematic lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homematic/ diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index cbabaafd3fb..07ba069d831 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -228,14 +228,7 @@ class HueLight(Light): @property def unique_id(self): """Return the ID of this Hue light.""" - lid = self.info.get('uniqueid') - - if lid is None: - default_type = 'Group' if self.is_group else 'Light' - ltype = self.info.get('type', default_type) - lid = '{}.{}.{}'.format(self.name, ltype, self.light_id) - - return '{}.{}'.format(self.__class__, lid) + return self.info.get('uniqueid') @property def name(self): diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index a2eed36a089..ba78546cf77 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the iGlo lighs.""" + """Set up the iGlo lights.""" host = config.get(CONF_HOST) name = config.get(CONF_NAME) port = config.get(CONF_PORT) diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py index f23ae77c8b2..ead0f153562 100644 --- a/homeassistant/components/light/ihc.py +++ b/homeassistant/components/light/ihc.py @@ -64,7 +64,7 @@ class IhcLight(IHCDevice, Light): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - dimmable=False, product: Element=None): + dimmable=False, product: Element=None) -> None: """Initialize the light.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._brightness = 0 diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index 88d621d4060..bd7814df8f3 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -57,7 +57,7 @@ class InsteonLocalDimmerDevice(Light): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @property def brightness(self): diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 51de9f03df5..f0ef0ce1b7e 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light): @callback def async_light_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from PLM for %s", self._address) + _LOGGER.info("Received update callback from PLM for %s", self._address) self._hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index a6191b05c7c..cee8155c322 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -27,7 +27,7 @@ def setup_platform(hass, config: ConfigType, class ISYLightDevice(ISYDevice, Light): - """Representation of an ISY994 light devie.""" + """Representation of an ISY994 light device.""" def __init__(self, node: object) -> None: """Initialize the ISY994 light device.""" diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 732cfe2a644..8c9e78ab2b0 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -57,7 +57,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up light for KNX platform configured within plattform.""" + """Set up light for KNX platform configured within platform.""" import xknx light = xknx.devices.Light( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 090341e4255..71a261e3806 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -397,6 +397,11 @@ class LIFXLight(Light): """Return the availability of the device.""" return self.registered + @property + def unique_id(self): + """Return a unique ID.""" + return self.device.mac_addr + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index aad2abdd183..0c6b1143bbd 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['limitlessled==1.0.8'] +REQUIREMENTS = ['limitlessled==1.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py index efc62b05434..576e244103f 100644 --- a/homeassistant/components/light/mochad.py +++ b/homeassistant/components/light/mochad.py @@ -62,7 +62,7 @@ class MochadLight(Light): @property def brightness(self): - """Return the birghtness of this light between 0..255.""" + """Return the brightness of this light between 0..255.""" return self._brightness def _get_device_status(self): diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index ed7ba1978cc..d4f2b93e6b5 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -44,11 +44,6 @@ LIGHT_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) -LIGHT_SCHEMA = vol.All( - cv.deprecated(CONF_ENTITY_ID), - LIGHT_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), }) diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index 07d4b63e99a..c39748e4430 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -70,7 +70,7 @@ class TikteckLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 30ad3a4d268..6aee02ee914 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -75,7 +75,7 @@ def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" - def __init__(self, smartbulb: 'SmartBulb', name): + def __init__(self, smartbulb: 'SmartBulb', name) -> None: """Initialize the bulb.""" self.smartbulb = smartbulb self._name = name diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 693e40c0292..540c718b04d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -76,8 +76,7 @@ class WemoLight(Light): @property def unique_id(self): """Return the ID of this light.""" - deviceid = self.device.uniqueID - return '{}.{}'.format(self.__class__, deviceid) + return self.device.uniqueID @property def name(self): @@ -176,7 +175,7 @@ class WemoDimmer(Light): @property def unique_id(self): """Return the ID of this WeMo dimmer.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index d1664d13072..efe37d3d577 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -51,13 +51,13 @@ class XiaomiGatewayLight(XiaomiDevice, Light): return True rgbhexstr = "%x" % value - if len(rgbhexstr) == 7: - rgbhexstr = '0' + rgbhexstr - elif len(rgbhexstr) != 8: - _LOGGER.error('Light RGB data error.' - ' Must be 8 characters. Received: %s', rgbhexstr) + if len(rgbhexstr) > 8: + _LOGGER.error("Light RGB data error." + " Can't be more than 8 characters. Received: %s", + rgbhexstr) return False + rgbhexstr = rgbhexstr.zfill(8) rgbhex = bytes.fromhex(rgbhexstr) rgba = struct.unpack('BBBB', rgbhex) brightness = rgba[0] diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 43c8860e77b..a3c5fa9f62e 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/light.xiaomi_miio/ import asyncio from functools import partial import logging +from math import ceil import voluptuous as vol @@ -29,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] # The light does not accept cct values < 1 CCT_MIN = 1 @@ -204,11 +205,11 @@ class XiaomiPhilipsGenericLight(Light): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = int(100 * brightness / 255) + percent_brightness = ceil(100 * brightness / 255.0) _LOGGER.debug( "Setting brightness: %s %s%%", - self.brightness, percent_brightness) + brightness, percent_brightness) result = yield from self._try_command( "Setting brightness failed: %s", @@ -235,7 +236,7 @@ class XiaomiPhilipsGenericLight(Light): _LOGGER.debug("Got new state: %s", state) self._state = state.is_on - self._brightness = int(255 * 0.01 * state.brightness) + self._brightness = ceil((255/100.0) * state.brightness) except DeviceException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) @@ -306,11 +307,11 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = int(100 * brightness / 255) + percent_brightness = ceil(100 * brightness / 255.0) _LOGGER.debug( "Setting brightness: %s %s%%", - self.brightness, percent_brightness) + brightness, percent_brightness) result = yield from self._try_command( "Setting brightness failed: %s", @@ -331,7 +332,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): _LOGGER.debug("Got new state: %s", state) self._state = state.is_on - self._brightness = int(255 * 0.01 * state.brightness) + self._brightness = ceil((255/100.0) * state.brightness) self._color_temp = self.translate( state.color_temperature, CCT_MIN, CCT_MAX, diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index c31bfec4927..33c84df14be 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -175,11 +175,6 @@ class YeelightLight(Light): """Return the list of supported effects.""" return YEELIGHT_EFFECT_LIST - @property - def unique_id(self) -> str: - """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._ipaddr) - @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index b453218c7c9..7071c8c43bb 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -67,7 +67,7 @@ class ZenggeLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index c468d50ce6d..f50b3d7689b 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -61,7 +61,7 @@ class Light(zha.Entity, light.Light): self._xy_color = None self._brightness = None - import bellows.zigbee.zcl.clusters as zcl_clusters + import zigpy.zcl.clusters as zcl_clusters if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters: self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_TRANSITION diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py index 8b9ad0209da..ea4df658ef6 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc.py @@ -1,5 +1,5 @@ """ -LIRC interface to receive signals from a infrared remote control. +LIRC interface to receive signals from an infrared remote control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lirc/ diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 80abce4ec3e..d03bbebd696 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -41,6 +41,11 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ _LOGGER = logging.getLogger(__name__) +PROP_TO_ATTR = { + 'changed_by': ATTR_CHANGED_BY, + 'code_format': ATTR_CODE_FORMAT, +} + @bind_hass def is_locked(hass, entity_id=None): @@ -156,12 +161,11 @@ class LockDevice(Entity): @property def state_attributes(self): """Return the state attributes.""" - if self.code_format is None: - return None - state_attr = { - ATTR_CODE_FORMAT: self.code_format, - ATTR_CHANGED_BY: self.changed_by - } + state_attr = {} + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value is not None: + state_attr[attr] = value return state_attr @property diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1dc0861d737..9e1e2e54ad9 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -116,7 +116,7 @@ class LogbookView(HomeAssistantView): extra_urls = ['/api/logbook/{datetime}'] def __init__(self, config): - """Initilalize the logbook view.""" + """Initialize the logbook view.""" self.config = config @asyncio.coroutine diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 21898f7b16d..c2309401977 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -93,7 +93,7 @@ def async_setup(hass, config): if LOGGER_LOGS in logfilter: logs.update(logfilter[LOGGER_LOGS]) - # Add new logpoints mapped to correc severity + # Add new logpoints mapped to correct severity for key, value in logpoints.items(): logs[key] = LOGSEVERITY[value] diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index a1e68555649..8ff3746889e 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -133,7 +133,7 @@ class MailboxEntity(Entity): class Mailbox(object): - """Represent an mailbox device.""" + """Represent a mailbox device.""" def __init__(self, hass, name): """Initialize mailbox object.""" diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py index a1953839f4f..2e807058edf 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/mailbox/asterisk_mbox.py @@ -30,7 +30,7 @@ class AsteriskMailbox(Mailbox): """Asterisk VM Sensor.""" def __init__(self, hass, name): - """Initialie Asterisk mailbox.""" + """Initialize Asterisk mailbox.""" super().__init__(hass, name) async_dispatcher_connect( self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index de56c5140e9..f712007ccec 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.01.14'] +REQUIREMENTS = ['youtube_dl==2018.01.21'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 91bcb4d8af0..06e89548785 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -88,6 +88,7 @@ MEDIA_TYPE_VIDEO = 'movie' MEDIA_TYPE_EPISODE = 'episode' MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' +MEDIA_TYPE_URL = 'url' SUPPORT_PAUSE = 1 SUPPORT_SEEK = 2 diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 293c6e51d52..474751c2574 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -49,7 +49,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from AVR: %s", message) + _LOGGER.info("Received update callback from AVR: %s", message) hass.async_add_job(device.async_update_ha_state()) avr = yield from anthemav.Connection.create( diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index ca6b152a37e..848c6abe91f 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BluesoundPlayer(MediaPlayerDevice): - """Represenatation of a Bluesound Player.""" + """Representation of a Bluesound Player.""" def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" @@ -150,10 +150,10 @@ class BluesoundPlayer(MediaPlayerDevice): self._port = DEFAULT_PORT @staticmethod - def _try_get_index(string, seach_string): + def _try_get_index(string, search_string): """Get the index.""" try: - return string.index(seach_string) + return string.index(search_string) except ValueError: return -1 @@ -165,7 +165,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: resp = yield from self.send_bluesound_command( 'SyncStatus', raise_timeout, raise_timeout) - except: + except Exception: raise if not resp: @@ -202,7 +202,7 @@ class BluesoundPlayer(MediaPlayerDevice): except CancelledError: _LOGGER.debug("Stopping the polling of node %s", self._name) - except: + except Exception: _LOGGER.exception("Unexpected error in %s", self._name) raise @@ -229,7 +229,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.info("Node %s is offline, retrying later", self.host) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION) - except: + except Exception: _LOGGER.exception("Unexpected when initiating error in %s", self.host) raise @@ -338,7 +338,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine @Throttle(UPDATE_CAPTURE_INTERVAL) def async_update_captures(self): - """Update Capture cources.""" + """Update Capture sources.""" resp = yield from self.send_bluesound_command( 'RadioBrowse?service=Capture') if not resp: diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 2aaff646885..928062cb2dc 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -205,7 +205,7 @@ class CastDevice(MediaPlayerDevice): @property def media_album_artist(self): - """Album arist of current playing media (Music track only).""" + """Album artist of current playing media (Music track only).""" return self.media_status.album_artist if self.media_status else None @property diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index d9688badcd1..057a23579ca 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -97,7 +97,7 @@ class ClementineDevice(MediaPlayerDevice): self._track_artist = client.current_track['track_artist'] self._track_album_name = client.current_track['track_album'] - except: + except Exception: self._state = STATE_OFF raise diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index fe25422360c..bcbee5c4ff7 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -77,7 +77,7 @@ class CmusDevice(MediaPlayerDevice): """Get the latest data and update the state.""" status = self.cmus.get_status_dict() if not status: - _LOGGER.warning("Recieved no status from cmus") + _LOGGER.warning("Received no status from cmus") else: self.status = status diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 572405baa6e..d85bd51e7fb 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -108,7 +108,7 @@ class DenonDevice(MediaPlayerDevice): if not line: break lines.append(line.decode('ASCII').strip()) - _LOGGER.debug("Recived: %s", line) + _LOGGER.debug("Received: %s", line) if all_lines: return lines diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index ebb8a670488..a3fe62c5a42 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -182,7 +182,7 @@ class EmbyDevice(MediaPlayerDevice): @property def unique_id(self): """Return the id of this emby client.""" - return '{}.{}'.format(self.__class__, self.device_id) + return self.device_id @property def supports_remote_control(self): @@ -273,7 +273,7 @@ class EmbyDevice(MediaPlayerDevice): @property def media_season(self): - """Season of curent playing media (TV Show only).""" + """Season of current playing media (TV Show only).""" return self.device.media_season @property diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py index 7054c83d36a..e1fffefed18 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -32,9 +32,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CecPlayerDevice(CecDevice, MediaPlayerDevice): - """Representation of a HDMI device as a Media palyer.""" + """Representation of a HDMI device as a Media player.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, hass, device, logical) self.entity_id = "%s.%s_%s" % ( diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 575ea414fa3..ca0979f1752 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -29,7 +29,7 @@ DOMAIN = 'itunes' SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_TURN_OFF SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -115,6 +115,10 @@ class Itunes(object): """Skip back and returns the current state.""" return self._command('previous') + def stop(self): + """Stop playback and return the current state.""" + return self._command('stop') + def play_playlist(self, playlist_id_or_name): """Set a playlist to be current and returns the current state.""" response = self._request('GET', '/playlists') @@ -280,7 +284,7 @@ class ItunesDevice(MediaPlayerDevice): """Image url of current playing media.""" if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \ self.current_title is not None: - return self.client.artwork_url() + return self.client.artwork_url() + '?id=' + self.content_id return 'https://cloud.githubusercontent.com/assets/260/9829355' \ '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' @@ -346,6 +350,11 @@ class ItunesDevice(MediaPlayerDevice): response = self.client.play_playlist(media_id) self.update_state(response) + def turn_off(self): + """Turn the media player off.""" + response = self.client.stop() + self.update_state(response) + class AirPlayDevice(MediaPlayerDevice): """Representation an AirPlay device via an iTunes API instance.""" diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py new file mode 100644 index 00000000000..3cf0ecdb232 --- /dev/null +++ b/homeassistant/components/media_player/mediaroom.py @@ -0,0 +1,201 @@ +""" +Support for the Mediaroom Set-up-box. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mediaroom/ +""" +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) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, + STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, + STATE_ON) +import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['pymediaroom==0.5'] + +_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" + +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, +}) + + +def setup_platform(hass, config, 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") + + from pymediaroom import Remote + + host = Remote.discover(known_hosts) + if host is None: + _LOGGER.warning("Can't find any STB") + return + hosts.append(host) + known_hosts.append(host) + + stbs = [] + + try: + for host in hosts: + stbs.append(MediaroomDevice( + config.get(CONF_NAME), + host, + config.get(CONF_OPTIMISTIC), + config.get(CONF_TIMEOUT) + )) + + 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) + + +class MediaroomDevice(MediaPlayerDevice): + """Representation of a Mediaroom set-up-box on the network.""" + + def __init__(self, name, host, 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._optimistic = optimistic + self._state = STATE_STANDBY + + def update(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) + + def 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) + if media_type != MEDIA_TYPE_CHANNEL: + _LOGGER.error('invalid media type') + return + if media_id.isdigit(): + media_id = int(media_id) + else: + return + self.stb.send_cmd(media_id) + self._state = STATE_PLAYING + + @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.""" + return self._state + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_MEDIAROOM + + @property + def media_content_type(self): + """Return the content type of current playing media.""" + return MEDIA_TYPE_CHANNEL + + def turn_on(self): + """Turn on the receiver.""" + self.stb.send_cmd('Power') + self._state = STATE_ON + + def turn_off(self): + """Turn off the receiver.""" + self.stb.send_cmd('Power') + self._state = STATE_STANDBY + + def media_play(self): + """Send play command.""" + _LOGGER.debug("media_play()") + self.stb.send_cmd('PlayPause') + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self.stb.send_cmd('PlayPause') + self._state = STATE_PAUSED + + def media_stop(self): + """Send stop command.""" + self.stb.send_cmd('Stop') + self._state = STATE_PAUSED + + def media_previous_track(self): + """Send Program Down command.""" + self.stb.send_cmd('ProgDown') + self._state = STATE_PLAYING + + def media_next_track(self): + """Send Program Up command.""" + self.stb.send_cmd('ProgUp') + self._state = STATE_PLAYING + + def volume_up(self): + """Send volume up command.""" + self.stb.send_cmd('VolUp') + + def volume_down(self): + """Send volume up command.""" + self.stb.send_cmd('VolDown') + + def mute_volume(self, mute): + """Send mute command.""" + self.stb.send_cmd('Mute') diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index d26fce0ea88..44d19ac6860 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MonopriceZone(MediaPlayerDevice): - """Representation of a a Monoprice amplifier zone.""" + """Representation of a Monoprice amplifier zone.""" def __init__(self, monoprice, sources, zone_id, zone_name): """Initialize new zone.""" diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 97ebe5be92b..432d9ce108f 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -6,6 +6,9 @@ https://home-assistant.io/components/media_player.onkyo/ """ import logging +# pylint: disable=unused-import +from typing import List # noqa: F401 + import voluptuous as vol from homeassistant.components.media_player import ( diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 8c946ec0f0f..21a897f4d35 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -11,14 +11,15 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MEDIA_TYPE_URL, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['panasonic_viera==0.2', - 'wakeonlan==0.2.2'] +REQUIREMENTS = ['panasonic_viera==0.3', + 'wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -30,7 +31,8 @@ DEFAULT_PORT = 55000 SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_OFF | SUPPORT_PLAY + SUPPORT_TURN_OFF | SUPPORT_PLAY | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -51,6 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info: _LOGGER.debug('%s', discovery_info) + name = discovery_info.get('name') host = discovery_info.get('host') port = discovery_info.get('port') remote = RemoteControl(host, port) @@ -69,9 +72,9 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def __init__(self, mac, name, remote): """Initialize the Panasonic device.""" - from wakeonlan import wol + import wakeonlan # Save a reference to the imported class - self._wol = wol + self._wol = wakeonlan self._mac = mac self._name = name self._muted = False @@ -183,3 +186,19 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def media_previous_track(self): """Send the previous track command.""" self.send_key('NRC_REW-ONOFF') + + def play_media(self, media_type, media_id, **kwargs): + """Play media.""" + _LOGGER.debug("Play media: %s (%s)", media_id, media_type) + + if media_type == MEDIA_TYPE_URL: + try: + self._remote.open_webpage(media_id) + except (TimeoutError, OSError): + self._state = STATE_OFF + else: + _LOGGER.warning("Unsupported media_type: %s", media_type) + + def media_stop(self): + """Stop playback.""" + self.send_key('NRC_CANCEL-ONOFF') diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index d8450d31ea4..24981555007 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -12,10 +12,11 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) + 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) +from homeassistant.helpers.script import Script from homeassistant.util import Throttle REQUIREMENTS = ['ha-philipsjs==0.0.1'] @@ -30,14 +31,16 @@ SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY +CONF_ON_ACTION = 'turn_on_action' + DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = 'Philips TV' -BASE_URL = 'http://{0}:1925/1/{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_ON_ACTION): cv.SCRIPT_SCHEMA, }) @@ -48,16 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) + turn_on_action = config.get(CONF_ON_ACTION) tvapi = haphilipsjs.PhilipsTV(host) + on_script = Script(hass, turn_on_action) if turn_on_action else None - add_devices([PhilipsTV(tvapi, name)]) + add_devices([PhilipsTV(tvapi, name, on_script)]) class PhilipsTV(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" - def __init__(self, tv, name): + def __init__(self, tv, name, on_script): """Initialize the Philips TV.""" self._tv = tv self._name = name @@ -74,6 +79,7 @@ class PhilipsTV(MediaPlayerDevice): self._source_mapping = {} self._watching_tv = None self._channel_name = None + self._on_script = on_script @property def name(self): @@ -88,9 +94,10 @@ class PhilipsTV(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" + is_supporting_turn_on = SUPPORT_TURN_ON if self._on_script else 0 if self._watching_tv: - return SUPPORT_PHILIPS_JS_TV - return SUPPORT_PHILIPS_JS + return SUPPORT_PHILIPS_JS_TV | is_supporting_turn_on + return SUPPORT_PHILIPS_JS | is_supporting_turn_on @property def state(self): @@ -126,6 +133,11 @@ class PhilipsTV(MediaPlayerDevice): """Boolean if volume is currently muted.""" return self._muted + def turn_on(self): + """Turn on the device.""" + if self._on_script: + self._on_script.run() + def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index c96b0f3c2ae..b2a89341cf0 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -175,7 +175,7 @@ def setup_plexserver( else: plex_clients[machine_identifier].refresh(None, session) - for machine_identifier, client in plex_clients.items(): + for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: client.force_idle() @@ -459,8 +459,7 @@ class PlexClient(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" - return '{}.{}'.format(self.__class__, self.machine_identifier or - self.name) + return self.machine_identifier @property def name(self): diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 57f25873ae7..4afd578211e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -8,6 +8,9 @@ import logging import socket from datetime import timedelta +import sys + +import subprocess import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,7 +23,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util -REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==0.2.2'] +REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -89,13 +92,13 @@ class SamsungTVDevice(MediaPlayerDevice): """Initialize the Samsung device.""" from samsungctl import exceptions from samsungctl import Remote - from wakeonlan import wol + import wakeonlan # Save a reference to the imported classes self._exceptions_class = exceptions self._remote_class = Remote self._name = name self._mac = mac - self._wol = wol + self._wol = wakeonlan # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -122,12 +125,19 @@ class SamsungTVDevice(MediaPlayerDevice): def update(self): """Update state of device.""" + if sys.platform == 'win32': + _ping_cmd = ['ping', '-n 1', '-w', '1000', self._config['host']] + else: + _ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', + self._config['host']] + + ping = subprocess.Popen( + _ping_cmd, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self._config[CONF_TIMEOUT]) - sock.connect((self._config['host'], self._config['port'])) - self._state = STATE_ON - except socket.error: + ping.communicate() + self._state = STATE_ON if ping.returncode == 0 else STATE_OFF + except subprocess.CalledProcessError: self._state = STATE_OFF def get_remote(self): diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 22f701de1cc..1fd61b3ead1 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SERVICE_CALL_METHOD = 'squeezebox_call_method' -DATA_SQUEEZEBOX = 'squeexebox' +DATA_SQUEEZEBOX = 'squeezebox' + +KNOWN_SERVERS = 'squeezebox_known_servers' ATTR_PARAMETERS = 'parameters' @@ -67,6 +69,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the squeezebox platform.""" import socket + known_servers = hass.data.get(KNOWN_SERVERS) + if known_servers is None: + hass.data[KNOWN_SERVERS] = known_servers = set() + if DATA_SQUEEZEBOX not in hass.data: hass.data[DATA_SQUEEZEBOX] = [] @@ -92,6 +98,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): "Could not communicate with %s:%d: %s", host, port, error) return False + if ipaddr in known_servers: + return + + known_servers.add(ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) @@ -473,7 +483,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self.async_query('playlist', 'play', media_id) def _add_uri_to_playlist(self, media_id): - """Add a items to the existing playlist.""" + """Add an item to the existing playlist.""" return self.async_query('playlist', 'add', media_id) def async_set_shuffle(self, shuffle): diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index d3346495015..abd8252d813 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) + SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC) + from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv @@ -24,7 +26,7 @@ CONF_ARGUMENTS = 'arguments' DEFAULT_NAME = 'Vlc' SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, @@ -146,6 +148,11 @@ class VlcDevice(MediaPlayerDevice): self._vlc.pause() self._state = STATE_PAUSED + def media_stop(self): + """Send stop command.""" + self._vlc.stop() + self._state = STATE_IDLE + def play_media(self, media_type, media_id, **kwargs): """Play media from a URL or file.""" if not media_type == MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index eda0bc2b326..84b957533fe 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -3,7 +3,10 @@ Volumio Platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.volumio/ + +Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ +from datetime import timedelta import logging import asyncio import aiohttp @@ -13,11 +16,13 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) + SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST) from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -30,8 +35,10 @@ TIMEOUT = 10 SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | \ + SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE | SUPPORT_CLEAR_PLAYLIST +PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, @@ -63,6 +70,8 @@ class Volumio(MediaPlayerDevice): self._state = {} self.async_update() self._lastvol = self._state.get('volume', 0) + self._playlists = [] + self._currentplaylist = None @asyncio.coroutine def send_volumio_msg(self, method, params=None): @@ -96,6 +105,7 @@ class Volumio(MediaPlayerDevice): def async_update(self): """Update state.""" resp = yield from self.send_volumio_msg('getState') + yield from self._async_update_playlists() if resp is False: return self._state = resp.copy() @@ -157,7 +167,7 @@ class Volumio(MediaPlayerDevice): def volume_level(self): """Volume level of the media player (0..1).""" volume = self._state.get('volume', None) - if volume is not None: + if volume is not None and volume != "": volume = volume / 100 return volume @@ -171,6 +181,16 @@ class Volumio(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def source_list(self): + """Return the list of available input sources.""" + return self._playlists + + @property + def source(self): + """Name of the current input source.""" + return self._currentplaylist + @property def supported_features(self): """Flag of media commands that are supported.""" @@ -199,14 +219,42 @@ class Volumio(MediaPlayerDevice): return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': int(volume * 100)}) + def async_volume_up(self): + """Service to send the Volumio the command for volume up.""" + return self.send_volumio_msg( + 'commands', params={'cmd': 'volume', 'volume': 'plus'}) + + def async_volume_down(self): + """Service to send the Volumio the command for volume down.""" + return self.send_volumio_msg( + 'commands', params={'cmd': 'volume', 'volume': 'minus'}) + def async_mute_volume(self, mute): """Send mute command to media player.""" mutecmd = 'mute' if mute else 'unmute' if mute: - # mute is implemenhted as 0 volume, do save last volume level + # mute is implemented as 0 volume, do save last volume level self._lastvol = self._state['volume'] return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': mutecmd}) return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': self._lastvol}) + + def async_select_source(self, source): + """Choose a different available playlist and play it.""" + self._currentplaylist = source + return self.send_volumio_msg( + 'commands', params={'cmd': 'playplaylist', 'name': source}) + + def async_clear_playlist(self): + """Clear players playlist.""" + self._currentplaylist = None + return self.send_volumio_msg('commands', + params={'cmd': 'clearQueue'}) + + @asyncio.coroutine + @Throttle(PLAYLIST_UPDATE_INTERVAL) + def _async_update_playlists(self, **kwargs): + """Update available Volumio playlists.""" + self._playlists = yield from self.send_volumio_msg('listplaylists') diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 55179ed60a9..3ccd3c7dbe9 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -9,6 +9,9 @@ from datetime import timedelta import logging from urllib.parse import urlparse +# pylint: disable=unused-import +from typing import Dict # noqa: F401 + import voluptuous as vol from homeassistant.components.media_player import ( @@ -23,7 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script import homeassistant.util as util -REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2', 'wakeonlan==0.2.2'] +REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) @@ -171,6 +174,7 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_UNKNOWN self._source_list = {} self._app_list = {} + self._channel = None @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): @@ -186,10 +190,12 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF self._current_source = None self._current_source_id = None + self._channel = None if self._state is not STATE_OFF: self._muted = self._client.get_muted() self._volume = self._client.get_volume() + self._channel = self._client.get_current_channel() self._source_list = {} self._app_list = {} @@ -222,6 +228,7 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF self._current_source = None self._current_source_id = None + self._channel = None @property def name(self): @@ -258,6 +265,14 @@ class LgWebOSDevice(MediaPlayerDevice): """Content type of current playing media.""" return MEDIA_TYPE_CHANNEL + @property + def media_title(self): + """Title of current playing media.""" + if (self._channel is not None) and ('channelName' in self._channel): + return self._channel['channelName'] + else: + return None + @property def media_image_url(self): """Image url of current playing media.""" diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 577988bc58c..f102d8a490d 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -149,11 +149,6 @@ class YamahaDevice(MediaPlayerDevice): self._name = name self._zone = receiver.zone - @property - def unique_id(self): - """Return an unique ID.""" - return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) - def update(self): """Get the latest details from the device.""" self._play_status = self.receiver.play_status() @@ -310,7 +305,7 @@ class YamahaDevice(MediaPlayerDevice): NOTE: this might take a while, because the only API interface for setting the net radio station emulates button pressing and - navigating through the net radio menu hiearchy. And each sub + navigating through the net radio menu hierarchy. And each sub menu must be fetched by the receiver from the vtuner service. """ diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py new file mode 100644 index 00000000000..ae82b96222e --- /dev/null +++ b/homeassistant/components/melissa.py @@ -0,0 +1,44 @@ +""" +Support for Melissa climate. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/melissa/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ["py-melissa-climate==1.0.1"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "melissa" +DATA_MELISSA = 'MELISSA' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Melissa Climate component.""" + import melissa + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + api = melissa.Melissa(username=username, password=password) + hass.data[DATA_MELISSA] = api + + load_platform(hass, 'sensor', DOMAIN, {}) + load_platform(hass, 'climate', DOMAIN, {}) + return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py new file mode 100644 index 00000000000..a228486e2c8 --- /dev/null +++ b/homeassistant/components/mercedesme.py @@ -0,0 +1,154 @@ +""" +Support for MercedesME System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = { + 'doorsClosed': ['Doors closed'], + 'windowsClosed': ['Windows closed'], + 'locked': ['Doors locked'], + 'tireWarningLight': ['Tire Warning'] +} + +SENSORS = { + 'fuelLevelPercent': ['Fuel Level', '%'], + 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], + 'latestTrip': ['Latest Trip', None], + 'odometerKm': ['Odometer', LENGTH_KILOMETERS], + 'serviceIntervalDays': ['Next Service', 'days'] +} + +DATA_MME = 'mercedesme' +DOMAIN = 'mercedesme' + +NOTIFICATION_ID = 'mercedesme_integration_notification' +NOTIFICATION_TITLE = 'Mercedes me integration setup' + +SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=30): + vol.All(cv.positive_int, vol.Clamp(min=10)) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up MercedesMe System.""" + from mercedesmejsonpy.controller import Controller + from mercedesmejsonpy import Exceptions + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + mercedesme_api = Controller(username, password, scan_interval) + if not mercedesme_api.is_valid_session: + raise Exceptions.MercedesMeException(500) + hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) + except Exceptions.MercedesMeException as ex: + if ex.code == 401: + hass.components.persistent_notification.create( + "Error:
    Please check username and password." + "You will need to restart Home Assistant after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + else: + hass.components.persistent_notification.create( + "Error:
    Can't communicate with Mercedes me API.
    " + "Error code: {} Reason: {}" + "You will need to restart Home Assistant after fixing." + "".format(ex.code, ex.message), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + _LOGGER.error("Unable to communicate with Mercedes me API: %s", + ex.message) + return False + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def hub_refresh(event_time): + """Call Mercedes me API to refresh information.""" + _LOGGER.info("Updating Mercedes me component.") + hass.data[DATA_MME].data.update() + dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) + + track_time_interval( + hass, + hub_refresh, + timedelta(seconds=scan_interval)) + + return True + + +class MercedesMeHub(object): + """Representation of a base MercedesMe device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class MercedesMeEntity(Entity): + """Entity class for MercedesMe devices.""" + + def __init__(self, data, internal_name, sensor_name, vin, unit): + """Initialize the MercedesMe entity.""" + self._car = None + self._data = data + self._state = False + self._name = sensor_name + self._internal_name = internal_name + self._unit = unit + self._vin = vin + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + # If the method is made a callback this should be changed + # to the async version. Check core.callback + self.schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e61ed05ce10..5a0bf2af1c4 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -337,7 +337,7 @@ class MicrosoftFace(object): @asyncio.coroutine def call_api(self, method, function, data=None, binary=False, params=None): - """Make a api call.""" + """Make an api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} url = self._server_url.format(function) diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py index 3cc4eda7675..9f53f84e020 100644 --- a/homeassistant/components/mochad.py +++ b/homeassistant/components/mochad.py @@ -14,7 +14,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.const import (CONF_HOST, CONF_PORT) -REQUIREMENTS = ['pymochad==0.1.1'] +REQUIREMENTS = ['pymochad==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 91053b41bf6..390da7ed0e0 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -76,12 +76,12 @@ def is_socket_address(value): def has_parent_dir(value): - """Validate that value is in an existing directory which is writetable.""" + """Validate that value is in an existing directory which is writeable.""" parent = os.path.dirname(os.path.realpath(value)) is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK) if not is_dir_writable: raise vol.Invalid( - '{} directory does not exist or is not writetable'.format(parent)) + '{} directory does not exist or is not writeable'.format(parent)) return value diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 512819b7e74..37028decf71 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -183,7 +183,7 @@ class NestDevice(object): "Connection error logging into the nest web service.") def smoke_co_alarms(self): - """Generate a list of smoke co alarams.""" + """Generate a list of smoke co alarms.""" try: for structure in self.nest.structures: if structure.name in self.local_structure: diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 6ef758b7bb5..e7a727bc5e2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -112,13 +112,13 @@ class ApnsDevice(object): self.device_disabled = True def __eq__(self, other): - """Return the comparision.""" + """Return the comparison.""" if isinstance(other, self.__class__): return self.push_id == other.push_id and self.name == other.name return NotImplemented def __ne__(self, other): - """Return the comparision.""" + """Return the comparison.""" return not self.__eq__(other) diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py index b7e5b1b813a..1a2b65f958f 100644 --- a/homeassistant/components/notify/gntp.py +++ b/homeassistant/components/notify/gntp.py @@ -44,7 +44,8 @@ def get_service(hass, config, discovery_info=None): if config.get(CONF_APP_ICON) is None: icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", "www_static", "icons", "favicon-192x192.png") - app_icon = open(icon_file, 'rb').read() + with open(icon_file, 'rb') as file: + app_icon = file.read() else: app_icon = config.get(CONF_APP_ICON) diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py index c5dbcb0d4ad..d14d8dcf8ad 100644 --- a/homeassistant/components/notify/knx.py +++ b/homeassistant/components/notify/knx.py @@ -51,7 +51,7 @@ def async_get_service_discovery(hass, discovery_info): @callback def async_get_service_config(hass, config): - """Set up notification for KNX platform configured within plattform.""" + """Set up notification for KNX platform configured within platform.""" import xknx notification = xknx.devices.Notification( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 05f4c5d17f3..3eb492f7fa6 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -51,7 +51,7 @@ def async_get_service(hass, config, discovery_info=None): encryption = config.get(CONF_PROXY_SSL) if host.startswith('http://') or host.startswith('https://'): - host = host.lstrip('http://').lstrip('https://') + host = host[host.index('://') + 3:] _LOGGER.warning( "Kodi host name should no longer contain http:// See updated " "definitions here: " diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py index 78a600ab8d6..30068854f2e 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -5,20 +5,41 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.pushsafer/ """ import logging +import base64 +import mimetypes import requests +from requests.auth import HTTPBasicAuth import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, + PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://www.pushsafer.com/api' +_ALLOWED_IMAGES = ['image/gif', 'image/jpeg', 'image/png'] CONF_DEVICE_KEY = 'private_key' +CONF_TIMEOUT = 15 -DEFAULT_TIMEOUT = 10 +# Top level attributes in 'data' +ATTR_SOUND = 'sound' +ATTR_VIBRATION = 'vibration' +ATTR_ICON = 'icon' +ATTR_ICONCOLOR = 'iconcolor' +ATTR_URL = 'url' +ATTR_URLTITLE = 'urltitle' +ATTR_TIME2LIVE = 'time2live' +ATTR_PICTURE1 = 'picture1' + +# Attributes contained in picture1 +ATTR_PICTURE1_URL = 'url' +ATTR_PICTURE1_PATH = 'path' +ATTR_PICTURE1_USERNAME = 'username' +ATTR_PICTURE1_PASSWORD = 'password' +ATTR_PICTURE1_AUTH = 'auth' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DEVICE_KEY): cv.string, @@ -27,21 +48,118 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the Pushsafer.com notification service.""" - return PushsaferNotificationService(config.get(CONF_DEVICE_KEY)) + return PushsaferNotificationService(config.get(CONF_DEVICE_KEY), + hass.config.is_allowed_path) class PushsaferNotificationService(BaseNotificationService): """Implementation of the notification service for Pushsafer.com.""" - def __init__(self, private_key): + def __init__(self, private_key, is_allowed_path): """Initialize the service.""" self._private_key = private_key + self.is_allowed_path = is_allowed_path def send_message(self, message='', **kwargs): - """Send a message to a user.""" + """Send a message to specified target.""" + if kwargs.get(ATTR_TARGET) is None: + targets = ["a"] + _LOGGER.debug("No target specified. Sending push to all") + else: + targets = kwargs.get(ATTR_TARGET) + _LOGGER.debug("%s target(s) specified", len(targets)) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - payload = {'k': self._private_key, 't': title, 'm': message} - response = requests.get(_RESOURCE, params=payload, - timeout=DEFAULT_TIMEOUT) - if response.status_code != 200: - _LOGGER.error("Not possible to send notification") + data = kwargs.get(ATTR_DATA, {}) + + # Converting the specified image to base64 + picture1 = data.get(ATTR_PICTURE1) + picture1_encoded = "" + if picture1 is not None: + _LOGGER.debug("picture1 is available") + url = picture1.get(ATTR_PICTURE1_URL, None) + local_path = picture1.get(ATTR_PICTURE1_PATH, None) + username = picture1.get(ATTR_PICTURE1_USERNAME) + password = picture1.get(ATTR_PICTURE1_PASSWORD) + auth = picture1.get(ATTR_PICTURE1_AUTH) + + if url is not None: + _LOGGER.debug("Loading image from url %s", url) + picture1_encoded = self.load_from_url(url, username, + password, auth) + elif local_path is not None: + _LOGGER.debug("Loading image from file %s", local_path) + picture1_encoded = self.load_from_file(local_path) + else: + _LOGGER.warning("missing url or local_path for picture1") + else: + _LOGGER.debug("picture1 is not specified") + + payload = { + 'k': self._private_key, + 't': title, + 'm': message, + 's': data.get(ATTR_SOUND, ""), + 'v': data.get(ATTR_VIBRATION, ""), + 'i': data.get(ATTR_ICON, ""), + 'c': data.get(ATTR_ICONCOLOR, ""), + 'u': data.get(ATTR_URL, ""), + 'ut': data.get(ATTR_URLTITLE, ""), + 'l': data.get(ATTR_TIME2LIVE, ""), + 'p': picture1_encoded + } + + for target in targets: + payload['d'] = target + response = requests.post(_RESOURCE, data=payload, + timeout=CONF_TIMEOUT) + if response.status_code != 200: + _LOGGER.error("Pushsafer failed with: %s", response.text) + else: + _LOGGER.debug("Push send: %s", response.json()) + + @classmethod + def get_base64(cls, filebyte, mimetype): + """Convert the image to the expected base64 string of pushsafer.""" + if mimetype not in _ALLOWED_IMAGES: + _LOGGER.warning("%s is a not supported mimetype for images", + mimetype) + return None + + base64_image = base64.b64encode(filebyte).decode('utf8') + return "data:{};base64,{}".format(mimetype, base64_image) + + def load_from_url(self, url=None, username=None, password=None, auth=None): + """Load image/document/etc from URL.""" + if url is not None: + _LOGGER.debug("Downloading image from %s", url) + if username is not None and password is not None: + auth_ = HTTPBasicAuth(username, password) + response = requests.get(url, auth=auth_, + timeout=CONF_TIMEOUT) + else: + response = requests.get(url, timeout=CONF_TIMEOUT) + return self.get_base64(response.content, + response.headers['content-type']) + else: + _LOGGER.warning("url not found in param") + + return None + + def load_from_file(self, local_path=None): + """Load image/document/etc from a local path.""" + try: + if local_path is not None: + _LOGGER.debug("Loading image from local path") + if self.is_allowed_path(local_path): + file_mimetype = mimetypes.guess_type(local_path) + _LOGGER.debug("Detected mimetype %s", file_mimetype) + with open(local_path, "rb") as binary_file: + data = binary_file.read() + return self.get_base64(data, file_mimetype[0]) + else: + _LOGGER.warning("Local path not found in params!") + except OSError as error: + _LOGGER.error("Can't load from local path: %s", error) + + return None diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 6cb98e45274..c6f4fa0dd5f 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.6'] +REQUIREMENTS = ['TwitterAPI==2.4.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py index e4be19c53ed..6ddf00cf7d4 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe.py @@ -8,22 +8,28 @@ import asyncio import voluptuous as vol +from homeassistant.const import (CONF_ICON, CONF_URL) import homeassistant.helpers.config_validation as cv -DOMAIN = 'panel_iframe' DEPENDENCIES = ['frontend'] +DOMAIN = 'panel_iframe' + CONF_TITLE = 'title' -CONF_ICON = 'icon' -CONF_URL = 'url' + +CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." +CONF_RELATIVE_URL_REGEX = r'\A/' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ cv.slug: { vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, - # pylint: disable=no-value-for-parameter - vol.Required(CONF_URL): vol.Url(), + vol.Required(CONF_URL): vol.Any( + vol.Match( + CONF_RELATIVE_URL_REGEX, + msg=CONF_RELATIVE_URL_ERROR_MSG), + cv.url), }})}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 3000820d28c..71e8232e8c2 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -130,7 +130,7 @@ class CallRateDelayThrottle(object): it should not block the mainloop. """ - def __init__(self, hass, delay_seconds: float): + def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) self._queue = [] diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 7df990fa0e5..24b8c682d02 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -92,7 +92,7 @@ CONFIG_SCHEMA = vol.Schema({ # Flag for enabling/disabling the loading of the history from the database. -# This feature is turned off right now as it's tests are not 100% stable. +# This feature is turned off right now as its tests are not 100% stable. ENABLE_LOAD_HISTORY = False @@ -336,9 +336,9 @@ class DailyHistory(object): self._max_dict = dict() self.max = None - def add_measurement(self, value, timestamp=datetime.now()): + def add_measurement(self, value, timestamp=None): """Add a new measurement for a certain day.""" - day = timestamp.date() + day = (timestamp or datetime.now()).date() if value is None: return if self._days is None: diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index d5d6f657bc6..4d5f27082de 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -171,7 +171,7 @@ def setup(hass, config): def qs_callback(item): """Typically a button press or update signal.""" if qsusb is None: # Shutting down - _LOGGER.info("Botton press or updating signal done") + _LOGGER.info("Button press or updating signal done") return # If button pressed, fire a hass event diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py index 882731d4f2c..76dda6fd366 100644 --- a/homeassistant/components/rainbird.py +++ b/homeassistant/components/rainbird.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Set up the Rain Bird componenent.""" + """Set up the Rain Bird component.""" conf = config[DOMAIN] server = conf.get(CONF_HOST) password = conf.get(CONF_PASSWORD) @@ -38,8 +38,8 @@ def setup(hass, config): _LOGGER.debug("Rain Bird Controller set to: %s", server) - initialstatus = controller.currentIrrigation() - if initialstatus == -1: + initial_status = controller.currentIrrigation() + if initial_status == -1: _LOGGER.error("Error getting state. Possible configuration issues") return False diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 1c9524223e5..b2628f954fc 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -16,7 +16,7 @@ import queue import threading import time -from typing import Dict, Optional +from typing import Any, Dict, Optional # noqa: F401 import voluptuous as vol @@ -34,7 +34,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.1'] +REQUIREMENTS = ['sqlalchemy==1.2.2'] _LOGGER = logging.getLogger(__name__) @@ -76,10 +76,10 @@ FILTER_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: FILTER_SCHEMA.extend({ - vol.Inclusive(CONF_PURGE_KEEP_DAYS, 'purge'): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Inclusive(CONF_PURGE_INTERVAL, 'purge'): + vol.Optional(CONF_PURGE_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_PURGE_INTERVAL, default=1): + vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_DB_URL): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -122,6 +122,12 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_days = conf.get(CONF_PURGE_KEEP_DAYS) purge_interval = conf.get(CONF_PURGE_INTERVAL) + if keep_days is None and purge_interval != 0: + _LOGGER.warning( + "From version 0.64.0 the 'recorder' component will by default " + "purge data older than 10 days. To keep data longer you must " + "configure 'purge_keep_days' or 'purge_interval'.") + db_url = conf.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( @@ -162,6 +168,7 @@ class Recorder(threading.Thread): self.hass = hass self.keep_days = keep_days self.purge_interval = purge_interval + self.did_vacuum = False self.queue = queue.Queue() # type: Any self.recording_start = dt_util.utcnow() self.db_url = uri diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index fad6a7de70d..06bd81c2309 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -55,11 +55,12 @@ def purge_old_data(instance, purge_days): # Execute sqlite vacuum command to free up space on disk _LOGGER.debug("DB engine driver: %s", instance.engine.driver) - if instance.engine.driver == 'pysqlite': + if instance.engine.driver == 'pysqlite' and not instance.did_vacuum: from sqlalchemy import exc _LOGGER.info("Vacuuming SQLite to free space") try: instance.engine.execute("VACUUM") + instance.did_vacuum = True except exc.OperationalError as err: _LOGGER.error("Error vacuuming SQLite: %s.", err) diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py index 7a04949dbeb..42d4ce77054 100644 --- a/homeassistant/components/remote/kira.py +++ b/homeassistant/components/remote/kira.py @@ -48,8 +48,8 @@ class KiraRemote(Entity): def send_command(self, command, **kwargs): """Send a command to one device.""" - for singel_command in command: - code_tuple = (singel_command, + for single_command in command: + code_tuple = (single_command, kwargs.get(remote.ATTR_DEVICE)) _LOGGER.info("Sending Command: %s to %s", *code_tuple) self._kira.sendCode(code_tuple) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 2a1deebdc7b..25ad626f96d 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -49,3 +49,16 @@ harmony_sync: entity_id: description: Name(s) of entities to sync. example: 'remote.family_room' + +xiaomi_miio_learn_command: + description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + fields: + entity_id: + description: 'Name of the entity to learn command from.' + example: 'remote.xiaomi_miio' + slot: + description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' + example: '1' + timeout: + description: 'Define the timeout in seconds, before which the command must be learned.' + example: '30' diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py new file mode 100644 index 00000000000..aa05246c9cd --- /dev/null +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -0,0 +1,255 @@ +""" +Support for the Xiaomi IR Remote (Chuangmi IR). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/remote.xiaomi_miio/ +""" +import asyncio +import logging +import time + +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.remote import ( + PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS, + DEFAULT_DELAY_SECS, RemoteDevice) +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, + ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) +import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utcnow + +REQUIREMENTS = ['python-miio==0.3.5'] + +_LOGGER = logging.getLogger(__name__) + +SERVICE_LEARN = 'xiaomi_miio_learn_command' +PLATFORM = 'xiaomi_miio' + +CONF_SLOT = 'slot' +CONF_COMMANDS = 'commands' + +DEFAULT_TIMEOUT = 10 +DEFAULT_SLOT = 1 + +LEARN_COMMAND_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): vol.All(str), + vol.Optional(CONF_TIMEOUT, default=10): + vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=1): + vol.All(int, vol.Range(min=1, max=1000000)), +}) + +COMMAND_SCHEMA = vol.Schema({ + vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string]) + }) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): + vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): + vol.All(int, vol.Range(min=1, max=1000000)), + vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, + vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), + vol.Optional(CONF_COMMANDS, default={}): + vol.Schema({cv.slug: COMMAND_SCHEMA}), +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" + from miio import ChuangmiIr, DeviceException + + host = config.get(CONF_HOST) + token = config.get(CONF_TOKEN) + + # Create handler + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + device = ChuangmiIr(host, token) + + # Check that we can communicate with device. + try: + device.info() + except DeviceException as ex: + _LOGGER.error("Token not accepted by device : %s", ex) + return + + if PLATFORM not in hass.data: + hass.data[PLATFORM] = {} + + friendly_name = config.get(CONF_NAME, "xiaomi_miio_" + + host.replace('.', '_')) + slot = config.get(CONF_SLOT) + timeout = config.get(CONF_TIMEOUT) + + hidden = config.get(ATTR_HIDDEN) + + xiaomi_miio_remote = XiaomiMiioRemote( + friendly_name, device, slot, timeout, + hidden, config.get(CONF_COMMANDS)) + + hass.data[PLATFORM][host] = xiaomi_miio_remote + + async_add_devices([xiaomi_miio_remote]) + + @asyncio.coroutine + def async_service_handler(service): + """Handle a learn command.""" + if service.service != SERVICE_LEARN: + _LOGGER.error("We should not handle service: %s", service.service) + return + + entity_id = service.data.get(ATTR_ENTITY_ID) + entity = None + for remote in hass.data[PLATFORM].values(): + if remote.entity_id == entity_id: + entity = remote + + if not entity: + _LOGGER.error("entity_id: '%s' not found", entity_id) + return + + device = entity.device + + slot = service.data.get(CONF_SLOT, entity.slot) + + yield from hass.async_add_job(device.learn, slot) + + timeout = service.data.get(CONF_TIMEOUT, entity.timeout) + + _LOGGER.info("Press the key you want Home Assistant to learn") + start_time = utcnow() + while (utcnow() - start_time) < timedelta(seconds=timeout): + message = yield from hass.async_add_job( + device.read, slot) + _LOGGER.debug("Message recieved from device: '%s'", message) + + if 'code' in message and message['code']: + log_msg = "Received command is: {}".format(message['code']) + _LOGGER.info(log_msg) + hass.components.persistent_notification.async_create( + log_msg, title='Xiaomi Miio Remote') + return + + if ('error' in message and + message['error']['message'] == "learn timeout"): + yield from hass.async_add_job(device.learn, slot) + + yield from asyncio.sleep(1, loop=hass.loop) + + _LOGGER.error("Timeout. No infrared command captured") + hass.components.persistent_notification.async_create( + "Timeout. No infrared command captured", + title='Xiaomi Miio Remote') + + hass.services.async_register(DOMAIN, SERVICE_LEARN, async_service_handler, + schema=LEARN_COMMAND_SCHEMA) + + +class XiaomiMiioRemote(RemoteDevice): + """Representation of a Xiaomi Miio Remote device.""" + + def __init__(self, friendly_name, device, + slot, timeout, hidden, commands): + """Initialize the remote.""" + self._name = friendly_name + self._device = device + self._is_hidden = hidden + self._slot = slot + self._timeout = timeout + self._state = False + self._commands = commands + + @property + def name(self): + """Return the name of the remote.""" + return self._name + + @property + def device(self): + """Return the remote object.""" + return self._device + + @property + def hidden(self): + """Return if we should hide entity.""" + return self._is_hidden + + @property + def slot(self): + """Return the slot to save learned command.""" + return self._slot + + @property + def timeout(self): + """Return the timeout for learning command.""" + return self._timeout + + @property + def is_on(self): + """Return False if device is unreachable, else True.""" + from miio import DeviceException + try: + self.device.info() + return True + except DeviceException: + return False + + @property + def should_poll(self): + """We should not be polled for device up state.""" + return False + + @property + def device_state_attributes(self): + """Hide remote by default.""" + if self._is_hidden: + return {'hidden': 'true'} + else: + return + + # pylint: disable=R0201 + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + _LOGGER.error("Device does not support turn_on, " + + "please use 'remote.send_command' to send commands.") + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the device off.""" + _LOGGER.error("Device does not support turn_off, " + + "please use 'remote.send_command' to send commands.") + + # pylint: enable=R0201 + def _send_command(self, payload): + """Send a command.""" + from miio import DeviceException + + _LOGGER.debug("Sending payload: '%s'", payload) + try: + self.device.play(payload) + except DeviceException as ex: + _LOGGER.error( + "Transmit of IR command failed, %s, exception: %s", + payload, ex) + + def send_command(self, command, **kwargs): + """Wrapper for _send_command.""" + num_repeats = kwargs.get(ATTR_NUM_REPEATS) + + delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + for _ in range(num_repeats): + for payload in command: + if payload in self._commands: + for local_payload in self._commands[payload][CONF_COMMAND]: + self._send_command(local_payload) + else: + self._send_command(payload) + time.sleep(delay) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 73922d56040..d97d4f38f02 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -390,7 +390,7 @@ class RflinkCommand(RflinkDevice): """Cancel queued signal repetition commands. For example when user changed state while repetitions are still - queued for broadcast. Or when a incoming Rflink command (remote + queued for broadcast. Or when an incoming Rflink command (remote switch) changes the state. """ # cancel any outstanding tasks from the previous state change diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 7d2e428c56b..de8a0c00d80 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -77,7 +77,7 @@ def setup(hass, config): """Set up the RFXtrx component.""" # Declare the Handle event def handle_receive(event): - """Handle revieved messages from RFXtrx gateway.""" + """Handle received messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: return @@ -171,7 +171,7 @@ def get_pt2262_cmd(device_id, data_bits): # pylint: disable=unused-variable def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" - for dev_id, device in RFX_DEVICES.items(): + for device in RFX_DEVICES.values(): if (hasattr(device, 'is_lighting4') and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)): diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py index 8c5c6570515..a7193b40949 100644 --- a/homeassistant/components/scsgate.py +++ b/homeassistant/components/scsgate.py @@ -87,7 +87,7 @@ class SCSGate(object): self._logger.debug("Received message {}".format(message)) if not isinstance(message, StateMessage) and \ not isinstance(message, ScenarioTriggeredMessage): - msg = "Ignored message {} - not releavant type".format( + msg = "Ignored message {} - not relevant type".format( message) self._logger.debug(msg) return @@ -109,7 +109,7 @@ class SCSGate(object): self._logger.error(msg) else: self._logger.info( - "Ignoring state message for device {} because unknonw".format( + "Ignoring state message for device {} because unknown".format( message.entity)) @property diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 20899396052..6b224492ffb 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -89,6 +89,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for symbol in symbols: try: + _LOGGER.debug("Configuring timeseries for symbols: %s", + symbol[CONF_SYMBOL]) timeseries.get_intraday(symbol[CONF_SYMBOL]) except ValueError: _LOGGER.error( @@ -100,6 +102,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from_cur = conversion.get(CONF_FROM) to_cur = conversion.get(CONF_TO) try: + _LOGGER.debug("Configuring forex %s - %s", + from_cur, to_cur) forex.get_currency_exchange_rate( from_currency=from_cur, to_currency=to_cur) except ValueError as error: @@ -110,6 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev.append(AlphaVantageForeignExchange(forex, conversion)) add_devices(dev, True) + _LOGGER.debug("Setup completed") class AlphaVantageSensor(Entity): @@ -158,8 +163,10 @@ class AlphaVantageSensor(Entity): def update(self): """Get the latest data and updates the states.""" + _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) self.values = next(iter(all_values.values())) + _LOGGER.debug("Received new values for symbol %s", self._symbol) class AlphaVantageForeignExchange(Entity): @@ -210,5 +217,11 @@ class AlphaVantageForeignExchange(Entity): def update(self): """Get the latest data and updates the states.""" + _LOGGER.debug("Requesting new data for forex %s - %s", + self._from_currency, + self._to_currency) self.values, _ = self._foreign_exchange.get_currency_exchange_rate( from_currency=self._from_currency, to_currency=self._to_currency) + _LOGGER.debug("Received new data for forex %s - %s", + self._from_currency, + self._to_currency) diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 44557978117..db7ab7c2e9e 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -61,11 +61,6 @@ class BlinkSensor(Entity): """Return the camera's current state.""" return self._state - @property - def unique_id(self): - """Return the unique camera sensor identifier.""" - return "sensor_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index 660cb5ede6e..ce44abdb087 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -64,7 +64,6 @@ class BloomSkySensor(Entity): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_sensor {}'.format(self._name) self._state = None @property @@ -72,11 +71,6 @@ class BloomSkySensor(Entity): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def state(self): """Return the current state, eg. value, of this sensor.""" diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index 09d8ec4659c..470d7749ea2 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -1,7 +1,7 @@ """ Support for BME680 Sensor over SMBus. -Temperature, humidity, pressure and volitile gas support. +Temperature, humidity, pressure and volatile gas support. Air Quality calculation based on humidity and volatile gas. For more details about this platform, please refer to the documentation at @@ -238,7 +238,7 @@ class BME680Handler: self._gas_sensor_running = True - # Pause to allow inital data read for device validation. + # Pause to allow initial data read for device validation. sleep(1) start_time = time() diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index d23236c2df8..1440e2496fe 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -129,7 +129,7 @@ class BroadlinkData(object): if retry < 1: _LOGGER.error(error) return - except vol.Invalid: + except (vol.Invalid, vol.MultipleInvalid): pass # Continue quietly if device returned malformed data if retry > 0 and self._auth(): self._update(retry-1) diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index b0d2c27ae5d..ded8f36203e 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -4,13 +4,27 @@ Support for Canary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.canary/ """ + from homeassistant.components.canary import DATA_CANARY -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity DEPENDENCIES = ['canary'] -SENSOR_VALUE_PRECISION = 1 +SENSOR_VALUE_PRECISION = 2 +ATTR_AIR_QUALITY = "air_quality" + +# Sensor types are defined like so: +# sensor type name, unit_of_measurement, icon +SENSOR_TYPES = [ + ["temperature", TEMP_CELSIUS, "mdi:thermometer"], + ["humidity", "%", "mdi:water-percent"], + ["air_quality", None, "mdi:weather-windy"], +] + +STATE_AIR_QUALITY_NORMAL = "normal" +STATE_AIR_QUALITY_ABNORMAL = "abnormal" +STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" def setup_platform(hass, config, add_devices, discovery_info=None): @@ -18,11 +32,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = hass.data[DATA_CANARY] devices = [] - from canary.api import SensorType for location in data.locations: for device in location.devices: if device.is_online: - for sensor_type in SensorType: + for sensor_type in SENSOR_TYPES: devices.append(CanarySensor(data, sensor_type, location, device)) @@ -37,10 +50,9 @@ class CanarySensor(Entity): self._data = data self._sensor_type = sensor_type self._device_id = device.device_id - self._is_celsius = location.is_celsius self._sensor_value = None - sensor_type_name = sensor_type.value.replace("_", " ").title() + sensor_type_name = sensor_type[0].replace("_", " ").title() self._name = '{} {} {}'.format(location.name, device.name, sensor_type_name) @@ -58,28 +70,51 @@ class CanarySensor(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return "sensor_canary_{}_{}".format(self._device_id, - self._sensor_type.value) + return "{}_{}".format(self._device_id, self._sensor_type[0]) @property def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - from canary.api import SensorType - if self._sensor_type == SensorType.TEMPERATURE: - return TEMP_CELSIUS if self._is_celsius else TEMP_FAHRENHEIT - elif self._sensor_type == SensorType.HUMIDITY: - return "%" - elif self._sensor_type == SensorType.AIR_QUALITY: - return "" + """Return the unit of measurement.""" + return self._sensor_type[1] + + @property + def icon(self): + """Icon for the sensor.""" + return self._sensor_type[2] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._sensor_type[0] == "air_quality" \ + and self._sensor_value is not None: + air_quality = None + if self._sensor_value <= .4: + air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL + elif self._sensor_value <= .59: + air_quality = STATE_AIR_QUALITY_ABNORMAL + elif self._sensor_value <= 1.0: + air_quality = STATE_AIR_QUALITY_NORMAL + + return { + ATTR_AIR_QUALITY: air_quality + } + return None def update(self): """Get the latest state of the sensor.""" self._data.update() - readings = self._data.get_readings(self._device_id) - value = next(( - reading.value for reading in readings - if reading.sensor_type == self._sensor_type), None) + from canary.api import SensorType + canary_sensor_type = None + if self._sensor_type[0] == "air_quality": + canary_sensor_type = SensorType.AIR_QUALITY + elif self._sensor_type[0] == "temperature": + canary_sensor_type = SensorType.TEMPERATURE + elif self._sensor_type[0] == "humidity": + canary_sensor_type = SensorType.HUMIDITY + + value = self._data.get_reading(self._device_id, canary_sensor_type) + if value is not None: self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION) diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py index d66c7d4e4b6..32e1d8f211a 100644 --- a/homeassistant/components/sensor/coinbase.py +++ b/homeassistant/components/sensor/coinbase.py @@ -4,21 +4,22 @@ Support for Coinbase sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.coinbase/ """ -from homeassistant.helpers.entity import Entity from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity - -DEPENDENCIES = ['coinbase'] - -DATA_COINBASE = 'coinbase_cache' - -CONF_ATTRIBUTION = "Data provided by coinbase.com" ATTR_NATIVE_BALANCE = "Balance in native currency" BTC_ICON = 'mdi:currency-btc' -ETH_ICON = 'mdi:currency-eth' + COIN_ICON = 'mdi:coin' +CONF_ATTRIBUTION = "Data provided by coinbase.com" + +DATA_COINBASE = 'coinbase_cache' +DEPENDENCIES = ['coinbase'] + +ETH_ICON = 'mdi:currency-eth' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Coinbase sensors.""" @@ -26,13 +27,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return if 'account' in discovery_info: account = discovery_info['account'] - sensor = AccountSensor(hass.data[DATA_COINBASE], - account['name'], - account['balance']['currency']) + sensor = AccountSensor( + hass.data[DATA_COINBASE], account['name'], + account['balance']['currency']) if 'exchange_currency' in discovery_info: - sensor = ExchangeRateSensor(hass.data[DATA_COINBASE], - discovery_info['exchange_currency'], - discovery_info['native_currency']) + sensor = ExchangeRateSensor( + hass.data[DATA_COINBASE], discovery_info['exchange_currency'], + discovery_info['native_currency']) add_devices([sensor], True) @@ -78,8 +79,8 @@ class AccountSensor(Entity): """Return the state attributes of the sensor.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_NATIVE_BALANCE: "{} {}".format(self._native_balance, - self._native_currency) + ATTR_NATIVE_BALANCE: "{} {}".format( + self._native_balance, self._native_currency), } def update(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 9d69583f673..f8ada07eec6 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==4.1.2'] +REQUIREMENTS = ['coinmarketcap==4.2.1'] _LOGGER = logging.getLogger(__name__) @@ -26,6 +26,7 @@ ATTR_MARKET_CAP = 'market_cap' ATTR_NAME = 'name' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' +ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PRICE = 'price' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' @@ -105,6 +106,7 @@ class CoinMarketCapSensor(Entity): 'market_cap_{}'.format(self.data.display_currency)), ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), + ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/sensor/comfoconnect.py index 9df28d861ee..ad6b07fb3da 100644 --- a/homeassistant/components/sensor/comfoconnect.py +++ b/homeassistant/components/sensor/comfoconnect.py @@ -96,7 +96,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type): + def __init__(self, hass, name, ccb: ComfoConnectBridge, + sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py index ad571110e88..0b2f6495b45 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/sensor/daikin.py @@ -57,7 +57,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DaikinClimateSensor(Entity): """Representation of a Sensor.""" - def __init__(self, api, monitored_state, units: UnitSystem, name=None): + def __init__(self, api, monitored_state, units: UnitSystem, + name=None) -> None: """Initialize the sensor.""" self._api = api self._sensor = SENSOR_TYPES.get(monitored_state) @@ -94,11 +95,6 @@ class DaikinClimateSensor(Entity): return value - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 39a258c5e6a..e224feb7db7 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -56,6 +56,9 @@ SENSOR_TYPES = { 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%', 'mdi:water-percent', ['currently', 'minutely', 'hourly', 'daily']], + 'precip_accumulation': ['Precip Accumulation', + 'cm', 'in', 'cm', 'cm', 'cm', 'mdi:weather-snowy', + ['hourly', 'daily']], 'temperature': ['Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly']], @@ -269,7 +272,8 @@ class DarkSkySensor(Entity): 'temperature_max', 'apparent_temperature_min', 'apparent_temperature_max', - 'precip_intensity_max']): + 'precip_intensity_max', + 'precip_accumulation']): self.forecast_data.update_daily() daily = self.forecast_data.data_daily if self.type == 'daily_summary': @@ -309,6 +313,7 @@ class DarkSkySensor(Entity): 'temperature_min', 'temperature_max', 'apparent_temperature_min', 'apparent_temperature_max', + 'precip_accumulation', 'pressure', 'ozone', 'uvIndex']): return round(state, 1) return state diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 7c2c1e0895f..b3adaa412ff 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -74,6 +74,11 @@ class DeconzSensor(Entity): """Return the name of the sensor.""" return self._sensor.name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return self._sensor.uniqueid + @property def device_class(self): """Return the class of the sensor.""" @@ -139,6 +144,11 @@ class DeconzBattery(Entity): """Return the name of the battery.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for the device.""" + return self._device.uniqueid + @property def device_class(self): """Return the class of the sensor.""" diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 278cf5382c1..2b125155892 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -14,12 +14,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.20'] +REQUIREMENTS = ['schiene==0.21'] _LOGGER = logging.getLogger(__name__) CONF_DESTINATION = 'to' CONF_START = 'from' +CONF_ONLY_DIRECT = 'only_direct' +DEFAULT_ONLY_DIRECT = False ICON = 'mdi:train' @@ -28,6 +30,7 @@ SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_START): cv.string, + vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean, }) @@ -35,17 +38,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Deutsche Bahn Sensor.""" start = config.get(CONF_START) destination = config.get(CONF_DESTINATION) + only_direct = config.get(CONF_ONLY_DIRECT) - add_devices([DeutscheBahnSensor(start, destination)], True) + add_devices([DeutscheBahnSensor(start, destination, only_direct)], True) class DeutscheBahnSensor(Entity): """Implementation of a Deutsche Bahn sensor.""" - def __init__(self, start, goal): + def __init__(self, start, goal, only_direct): """Initialize the sensor.""" self._name = '{} to {}'.format(start, goal) - self.data = SchieneData(start, goal) + self.data = SchieneData(start, goal, only_direct) self._state = None @property @@ -84,19 +88,21 @@ class DeutscheBahnSensor(Entity): class SchieneData(object): """Pull data from the bahn.de web page.""" - def __init__(self, start, goal): + def __init__(self, start, goal, only_direct): """Initialize the sensor.""" import schiene self.start = start self.goal = goal + self.only_direct = only_direct self.schiene = schiene.Schiene() self.connections = [{}] def update(self): """Update the connection data.""" self.connections = self.schiene.connections( - self.start, self.goal, dt_util.as_local(dt_util.utcnow())) + self.start, self.goal, dt_util.as_local(dt_util.utcnow()), + self.only_direct) if not self.connections: self.connections = [{}] @@ -108,6 +114,6 @@ class SchieneData(object): con.pop('details') delay = con.get('delay', {'delay_departure': 0, 'delay_arrival': 0}) - # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] + con['delay_arrival'] = delay['delay_arrival'] con['ontime'] = con.get('ontime', False) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 5b20ac0f4d0..32c888bad3b 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -87,14 +87,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(devices) def update_entities_telegram(telegram): - """Update entities with latests telegram and trigger state update.""" + """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram for device in devices: device.telegram = telegram hass.async_add_job(device.async_update_ha_state()) - # Creates a asyncio.Protocol factory for reading DSMR telegrams from serial - # and calls update_entities_telegram to update entities on arrival + # Creates an asyncio.Protocol factory for reading DSMR telegrams from + # serial and calls update_entities_telegram to update entities on arrival if config[CONF_HOST]: reader_factory = partial( create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT], @@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if transport: # Register listener to close transport on HA shutdown - stop_listerer = hass.bus.async_listen_once( + stop_listener = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, transport.close) # Wait for reader to close @@ -131,8 +131,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if hass.state != CoreState.stopping: # Unexpected disconnect if transport: - # remove listerer - stop_listerer() + # remove listener + stop_listener() # Reflect disconnect state in devices state by setting an # empty telegram resulting in `unknown` states diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index 27d4bdd2f5a..eee959fceba 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ebox_data = EBoxData(username, password) ebox_data.update() except requests.exceptions.HTTPError as error: - _LOGGER.error("Failt login: %s", error) + _LOGGER.error("Failed login: %s", error) return False name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index a0c6f7a92e4..dad770d5bab 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -50,18 +50,13 @@ class EcobeeSensor(Entity): @property def name(self): """Return the name of the Ecobee sensor.""" - return self._name.rstrip() + return self._name @property def state(self): """Return the state of the sensor.""" return self._state - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "sensor_ecobee_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fe05da3ccdd..ef06458cd84 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -122,7 +122,7 @@ class EddystoneTemp(Entity): class Monitor(object): - """Continously scan for BLE advertisements.""" + """Continuously scan for BLE advertisements.""" def __init__(self, hass, devices, bt_device_id): """Construct interface object.""" @@ -150,7 +150,7 @@ class Monitor(object): self.scanning = False def start(self): - """Continously scan for BLE advertisements.""" + """Continuously scan for BLE advertisements.""" if not self.scanning: self.scanner.start() self.scanning = True diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py index e6f4addf003..e0a42fdb6a8 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -65,7 +65,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class EightHeatSensor(EightSleepHeatEntity): - """Representation of a eight sleep heat-based sensor.""" + """Representation of an eight sleep heat-based sensor.""" def __init__(self, name, eight, sensor): """Initialize the sensor.""" @@ -116,7 +116,7 @@ class EightHeatSensor(EightSleepHeatEntity): class EightUserSensor(EightSleepUserEntity): - """Representation of a eight sleep user-based sensor.""" + """Representation of an eight sleep user-based sensor.""" def __init__(self, name, eight, sensor, units): """Initialize the sensor.""" @@ -232,7 +232,7 @@ class EightUserSensor(EightSleepUserEntity): class EightRoomSensor(EightSleepUserEntity): - """Representation of a eight sleep room sensor.""" + """Representation of an eight sleep room sensor.""" def __init__(self, name, eight, sensor, units): """Initialize the sensor.""" diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index fc1daf151c7..cd02137f4d5 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -153,7 +153,7 @@ class EmonCmsSensor(Entity): @property def device_state_attributes(self): - """Return the atrributes of the sensor.""" + """Return the attributes of the sensor.""" return { ATTR_FEEDID: self._elem["id"], ATTR_TAG: self._elem["tag"], diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 07c085cd18d..4fc79745b99 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -50,12 +50,12 @@ SENSOR_TYPES = { 'text_int_used': ['International text used', MESSAGES, 'mdi:message-alert'], 'text_int_limit': ['International text limit', - MESSAGES, 'mdi:message-alart'], - 'text_int_remaining': ['Internaltional remaining', + MESSAGES, 'mdi:message-alert'], + 'text_int_remaining': ['International remaining', MESSAGES, 'mdi:message-alert'], 'talk_used': ['Talk used', MINUTES, 'mdi:cellphone'], 'talk_limit': ['Talk limit', MINUTES, 'mdi:cellphone'], - 'talt_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'], + 'talk_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'], 'other_talk_used': ['Other Talk used', MINUTES, 'mdi:cellphone'], 'other_talk_limit': ['Other Talk limit', MINUTES, 'mdi:cellphone'], 'other_talk_remaining': ['Other Talk remaining', MINUTES, 'mdi:cellphone'], diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index ea6382ce795..b443bd56f03 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -70,8 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): phonebook = FritzBoxPhonebook( host=host, port=port, username=username, password=password, phonebook_id=phonebook_id, prefixes=prefixes) - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except phonebook = None _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", phonebook_id) diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py index 2bc7e5b3b3a..aa1d2d9eff0 100644 --- a/homeassistant/components/sensor/gearbest.py +++ b/homeassistant/components/sensor/gearbest.py @@ -1,5 +1,5 @@ """ -Parse prices of a item from gearbest. +Parse prices of an item from gearbest. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.gearbest/ diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index af31c14789a..cae2eaf7437 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -48,5 +48,5 @@ class HiveSensorEntity(Entity): return self.session.sensor.hub_online_status(self.node_id) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/sensor/ihc.py index 3ad86e51f97..b6440a407a4 100644 --- a/homeassistant/components/sensor/ihc.py +++ b/homeassistant/components/sensor/ihc.py @@ -62,7 +62,7 @@ class IHCSensor(IHCDevice, Entity): """Implementation of the IHC sensor.""" def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - unit, product: Element=None): + unit, product: Element=None) -> None: """Initialize the IHC sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 8adf85f0a2e..c0d492984e0 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -4,25 +4,25 @@ InfluxDB component which allows you to get data from an Influx database. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.influxdb/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL, - CONF_NAME, CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE) -from homeassistant.const import STATE_UNKNOWN -from homeassistant.util import Throttle +from homeassistant.components.influxdb import CONF_DB_NAME +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, STATE_UNKNOWN) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['influxdb==4.1.1'] +REQUIREMENTS = ['influxdb==5.0.0'] DEFAULT_HOST = 'localhost' DEFAULT_PORT = 8086 @@ -32,13 +32,13 @@ DEFAULT_VERIFY_SSL = False DEFAULT_GROUP_FUNCTION = 'mean' DEFAULT_FIELD = 'value' -CONF_DB_NAME = 'database' CONF_QUERIES = 'queries' CONF_GROUP_FUNCTION = 'group_function' CONF_FIELD = 'field' CONF_MEASUREMENT_NAME = 'measurement' CONF_WHERE = 'where' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _QUERY_SCHEME = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -62,9 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean }) -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the InfluxDB component.""" @@ -122,7 +119,7 @@ class InfluxSensor(Entity): except exceptions.InfluxDBClientError as exc: _LOGGER.error("Database host is not accessible due to '%s', please" " check your entries in the configuration file and" - " that the database exists and is READ/WRITE.", exc) + " that the database exists and is READ/WRITE", exc) self.connected = False @property diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index 9a23da48a6b..398c0b350ee 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -58,7 +58,7 @@ class IOSSensor(Entity): def unique_id(self): """Return the unique ID of this sensor.""" device_id = self._device[ios.ATTR_DEVICE_ID] - return "sensor_ios_battery_{}_{}".format(self.type, device_id) + return "{}_{}".format(self.type, device_id) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 76f026bba10..39c9d8a3b9d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -317,11 +317,6 @@ class ISYWeatherDevice(ISYDevice): """Initialize the ISY994 weather device.""" super().__init__(node) - @property - def unique_id(self) -> str: - """Return the unique identifier for the node.""" - return self._node.name - @property def raw_units(self) -> str: """Return the raw unit of measurement.""" diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 0a455099597..70afa6fe1e1 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -52,7 +52,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up sensor for KNX platform configured within plattform.""" + """Set up sensor for KNX platform configured within platform.""" import xknx sensor = xknx.devices.Sensor( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py new file mode 100644 index 00000000000..58313428861 --- /dev/null +++ b/homeassistant/components/sensor/melissa.py @@ -0,0 +1,98 @@ +""" +Support for Melissa climate Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.melissa/ +""" +import logging + +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['melissa'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the melissa sensor platform.""" + sensors = [] + api = hass.data[DATA_MELISSA] + devices = api.fetch_devices().values() + + for device in devices: + sensors.append(MelissaTemperatureSensor(device, api)) + sensors.append(MelissaHumiditySensor(device, api)) + add_devices(sensors) + + +class MelissaSensor(Entity): + """Representation of a Melissa Sensor.""" + + _type = 'generic' + + def __init__(self, device, api): + """Initialize the sensor.""" + self._api = api + self._state = None + self._name = '{0} {1}'.format( + device['name'], + self._type + ) + self._serial = device['serial_number'] + self._data = device['controller_log'] + + @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 + + def update(self): + """Fetch status from melissa.""" + self._data = self._api.status(cached=True) + + +class MelissaTemperatureSensor(MelissaSensor): + """Representation of a Melissa temperature Sensor.""" + + _type = 'temperature' + _unit = TEMP_CELSIUS + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data for the sensor.""" + super().update() + try: + self._state = self._data[self._serial]['temp'] + except KeyError: + _LOGGER.warning("Unable to get temperature for %s", self.entity_id) + + +class MelissaHumiditySensor(MelissaSensor): + """Representation of a Melissa humidity Sensor.""" + + _type = 'humidity' + _unit = '%' + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data for the sensor.""" + super().update() + try: + self._state = self._data[self._serial]['humidity'] + except KeyError: + _LOGGER.warning("Unable to get humidity for %s", self.entity_id) diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py new file mode 100644 index 00000000000..bc368745e40 --- /dev/null +++ b/homeassistant/components/sensor/mercedesme.py @@ -0,0 +1,83 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, SENSORS) + + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + if discovery_info is None: + return + + data = hass.data[DATA_MME].data + + if not data.cars: + return + + devices = [] + for car in data.cars: + for key, value in sorted(SENSORS.items()): + devices.append( + MercedesMESensor(data, key, value[0], car["vin"], value[1])) + + add_devices(devices, True) + + +class MercedesMESensor(MercedesMeEntity): + """Representation of a Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating %s", self._internal_name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "latestTrip": + self._state = self._car["latestTrip"]["id"] + else: + self._state = self._car[self._internal_name] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._internal_name == "latestTrip": + return { + "duration_seconds": + self._car["latestTrip"]["durationSeconds"], + "distance_traveled_km": + self._car["latestTrip"]["distanceTraveledKm"], + "started_at": datetime.datetime.fromtimestamp( + self._car["latestTrip"]["startedAt"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "average_speed_km_per_hr": + self._car["latestTrip"]["averageSpeedKmPerHr"], + "finished": self._car["latestTrip"]["finished"], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + return { + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 772e59f266e..b6366de6432 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -47,7 +47,7 @@ CONDITION_CLASSES = { DEFAULT_NAME = "Met Office" -VISIBILTY_CLASSES = { +VISIBILITY_CLASSES = { 'VP': '<1', 'PO': '1-4', 'MO': '4-10', @@ -144,7 +144,7 @@ class MetOfficeCurrentSensor(Entity): """Return the state of the sensor.""" if (self._condition == 'visibility_distance' and hasattr(self.data.data, 'visibility')): - return VISIBILTY_CLASSES.get(self.data.data.visibility.value) + return VISIBILITY_CLASSES.get(self.data.data.visibility.value) if hasattr(self.data.data, self._condition): variable = getattr(self.data.data, self._condition) if self._condition == 'weather': diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 56f8c3cfe47..ec68588f241 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) -REQUIREMENTS = ['miflora==0.2.0'] +REQUIREMENTS = ['miflora==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -138,12 +138,16 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ + from miflora.backends import BluetoothBackendException try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) except IOError as ioerr: _LOGGER.info("Polling error %s", ioerr) return + except BluetoothBackendException as bterror: + _LOGGER.info("Polling error %s", bterror) + return if data is not None: _LOGGER.debug("%s = %s", self.name, data) diff --git a/homeassistant/components/sensor/modem_callerid.py b/homeassistant/components/sensor/modem_callerid.py index 0b71540f346..f80ea5853c8 100644 --- a/homeassistant/components/sensor/modem_callerid.py +++ b/homeassistant/components/sensor/modem_callerid.py @@ -18,7 +18,7 @@ REQUIREMENTS = ['basicmodem==0.7'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Modem CallerID' -ICON = 'mdi:phone-clasic' +ICON = 'mdi:phone-classic' DEFAULT_DEVICE = '/dev/ttyACM0' STATE_RING = 'ring' diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 8ace931a8cc..c20e0a59408 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -113,8 +113,7 @@ class NetAtmoSensor(Entity): module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] - self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format( - self._name, module_id, self.type) + self._unique_id = '{}-{}'.format(self.module_id, self.type) @property def name(self): diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index 2228a8eab60..a9fb3ae7a6f 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -243,7 +243,7 @@ class PyNUTData(object): """ def __init__(self, host, port, alias, username, password): - """Initialize the data oject.""" + """Initialize the data object.""" from pynut2.nut2 import PyNUTClient, PyNUTError self._host = host self._port = port diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 1f58eb4c13e..8a07d3484d5 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -66,8 +66,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device_file, 'temperature')) else: for family_file_path in glob(os.path.join(base_dir, '*', 'family')): - family_file = open(family_file_path, "r") - family = family_file.read() + with open(family_file_path, "r") as family_file: + family = family_file.read() if family in DEVICE_SENSORS: for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): sensor_id = os.path.split( diff --git a/homeassistant/components/sensor/openevse.py b/homeassistant/components/sensor/openevse.py index 6ded982eea1..9086f37f8b2 100644 --- a/homeassistant/components/sensor/openevse.py +++ b/homeassistant/components/sensor/openevse.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'status': ['Charging Status', None], 'charge_time': ['Charge Time Elapsed', 'minutes'], - 'ambient_temp': ['Ambient Termperature', TEMP_CELSIUS], + 'ambient_temp': ['Ambient Temperature', TEMP_CELSIUS], 'ir_temp': ['IR Temperature', TEMP_CELSIUS], 'rtc_temp': ['RTC Temperature', TEMP_CELSIUS], 'usage_session': ['Usage this Session', 'kWh'], diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 86ca496eb62..5b5385f14ef 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -79,10 +79,10 @@ class PilightSensor(Entity): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ - # Check if received code matches defined playoad + # Check if received code matches defined payload # True if payload is contained in received code dict, not # all items have to match if self._payload.items() <= call.data.items(): diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py new file mode 100644 index 00000000000..3998af7e32f --- /dev/null +++ b/homeassistant/components/sensor/pollen.py @@ -0,0 +1,322 @@ +""" +Support for Pollen.com allergen and cold/flu sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.pollen/ +""" +import logging +from datetime import timedelta +from statistics import mean + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS +) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pypollencom==1.1.1'] +_LOGGER = logging.getLogger(__name__) + +ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' +ATTR_ALLERGEN_NAME = 'primary_allergen_name' +ATTR_ALLERGEN_TYPE = 'primary_allergen_type' +ATTR_CITY = 'city' +ATTR_OUTLOOK = 'outlook' +ATTR_RATING = 'rating' +ATTR_SEASON = 'season' +ATTR_TREND = 'trend' +ATTR_ZIP_CODE = 'zip_code' + +CONF_ZIP_CODE = 'zip_code' + +DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' + +MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12) +MIN_TIME_UPDATE_INDICES = timedelta(minutes=10) + +CONDITIONS = { + 'allergy_average_forecasted': ( + 'Allergy Index: Forecasted Average', + 'AllergyAverageSensor', + 'allergy_average_data', + {'data_attr': 'extended_data'}, + 'mdi:flower' + ), + 'allergy_average_historical': ( + 'Allergy Index: Historical Average', + 'AllergyAverageSensor', + 'allergy_average_data', + {'data_attr': 'historic_data'}, + 'mdi:flower' + ), + 'allergy_index_today': ( + 'Allergy Index: Today', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Today'}, + 'mdi:flower' + ), + 'allergy_index_tomorrow': ( + 'Allergy Index: Tomorrow', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Tomorrow'}, + 'mdi:flower' + ), + 'allergy_index_yesterday': ( + 'Allergy Index: Yesterday', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Yesterday'}, + 'mdi:flower' + ), + 'disease_average_forecasted': ( + 'Cold & Flu: Forecasted Average', + 'AllergyAverageSensor', + 'disease_average_data', + {'data_attr': 'extended_data'}, + 'mdi:snowflake' + ) +} + +RATING_MAPPING = [{ + 'label': 'Low', + 'minimum': 0.0, + 'maximum': 2.4 +}, { + 'label': 'Low/Medium', + 'minimum': 2.5, + 'maximum': 4.8 +}, { + 'label': 'Medium', + 'minimum': 4.9, + 'maximum': 7.2 +}, { + 'label': 'Medium/High', + 'minimum': 7.3, + 'maximum': 9.6 +}, { + 'label': 'High', + 'minimum': 9.7, + 'maximum': 12 +}] + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ZIP_CODE): cv.positive_int, + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Configure the platform and add the sensors.""" + from pypollencom import Client + + _LOGGER.debug('Configuration data: %s', config) + + client = Client(config[CONF_ZIP_CODE]) + datas = { + 'allergy_average_data': AllergyAveragesData(client), + 'allergy_index_data': AllergyIndexData(client), + 'disease_average_data': DiseaseData(client) + } + + for data in datas.values(): + data.update() + + sensors = [] + for condition in config[CONF_MONITORED_CONDITIONS]: + name, sensor_class, data_key, params, icon = CONDITIONS[condition] + sensors.append(globals()[sensor_class]( + datas[data_key], + params, + name, + icon + )) + + add_devices(sensors, True) + + +def calculate_trend(list_of_nums): + """Calculate the most common rating as a trend.""" + ratings = list( + r['label'] for n in list_of_nums + for r in RATING_MAPPING + if r['minimum'] <= n <= r['maximum']) + return max(set(ratings), key=ratings.count) + + +class BaseSensor(Entity): + """Define a base class for all of our sensors.""" + + def __init__(self, data, data_params, name, icon): + """Initialize the sensor.""" + self._attrs = {} + self._icon = icon + self._name = name + self._data_params = data_params + self._state = None + self._unit = None + self.data = data + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}) + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + +class AllergyAverageSensor(BaseSensor): + """Define a sensor to show allergy average information.""" + + def update(self): + """Update the status of the sensor.""" + self.data.update() + + data_attr = getattr(self.data, self._data_params['data_attr']) + indices = [ + p['Index'] + for p in data_attr['Location']['periods'] + ] + average = round(mean(indices), 1) + + self._attrs[ATTR_TREND] = calculate_trend(indices) + self._attrs[ATTR_CITY] = data_attr['Location']['City'].title() + self._attrs[ATTR_STATE] = data_attr['Location']['State'] + self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP'] + + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= average <= i['maximum'] + ] + self._attrs[ATTR_RATING] = rating + + self._state = average + self._unit = 'index' + + +class AllergyIndexSensor(BaseSensor): + """Define a sensor to show allergy index information.""" + + def update(self): + """Update the status of the sensor.""" + self.data.update() + + location_data = self.data.current_data['Location'] + [period] = [ + p for p in location_data['periods'] + if p['Type'] == self._data_params['key'] + ] + + self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus'] + self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name'] + self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0]['PlantType'] + self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook'] + self._attrs[ATTR_SEASON] = self.data.outlook_data['Season'] + self._attrs[ATTR_TREND] = self.data.outlook_data[ + 'Trend'].title() + self._attrs[ATTR_CITY] = location_data['City'].title() + self._attrs[ATTR_STATE] = location_data['State'] + self._attrs[ATTR_ZIP_CODE] = location_data['ZIP'] + + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= period['Index'] <= i['maximum'] + ] + self._attrs[ATTR_RATING] = rating + + self._state = period['Index'] + self._unit = 'index' + + +class DataBase(object): + """Define a generic data object.""" + + def __init__(self, client): + """Initialize.""" + self._client = client + + def _get_client_data(self, module, operation): + """Get data from a particular point in the API.""" + from pypollencom.exceptions import HTTPError + + try: + data = getattr(getattr(self._client, module), operation)() + _LOGGER.debug('Received "%s_%s" data: %s', module, + operation, data) + except HTTPError as exc: + _LOGGER.error('An error occurred while retrieving data') + _LOGGER.debug(exc) + + return data + + +class AllergyAveragesData(DataBase): + """Define an object to averages on future and historical allergy data.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.extended_data = None + self.historic_data = None + + @Throttle(MIN_TIME_UPDATE_AVERAGES) + def update(self): + """Update with new data.""" + self.extended_data = self._get_client_data('allergens', 'extended') + self.historic_data = self._get_client_data('allergens', 'historic') + + +class AllergyIndexData(DataBase): + """Define an object to retrieve current allergy index info.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.current_data = None + self.outlook_data = None + + @Throttle(MIN_TIME_UPDATE_INDICES) + def update(self): + """Update with new index data.""" + self.current_data = self._get_client_data('allergens', 'current') + self.outlook_data = self._get_client_data('allergens', 'outlook') + + +class DiseaseData(DataBase): + """Define an object to retrieve current disease index info.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.extended_data = None + + @Throttle(MIN_TIME_UPDATE_INDICES) + def update(self): + """Update with new cold/flu data.""" + self.extended_data = self._get_client_data('disease', 'extended') diff --git a/homeassistant/components/sensor/pushbullet.py b/homeassistant/components/sensor/pushbullet.py index 086698b06bf..415174ac273 100644 --- a/homeassistant/components/sensor/pushbullet.py +++ b/homeassistant/components/sensor/pushbullet.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Pushbllet Sensor platform.""" + """Set up the Pushbullet Sensor platform.""" from pushbullet import PushBullet from pushbullet import InvalidKeyError try: diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 20460f9063c..3caebad2007 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -197,7 +197,6 @@ class QNAPStatsAPI(object): self.data = {} - # pylint: disable=bare-except @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update API information and store locally.""" @@ -207,7 +206,7 @@ class QNAPStatsAPI(object): self.data["smart_drive_health"] = self._api.get_smart_disk_health() self.data["volumes"] = self._api.get_volumes() self.data["bandwidth"] = self._api.get_bandwidth() - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception("Failed to fetch QNAP stats from the NAS") diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index 3b2c818a7b3..8adaeb3c71b 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -162,7 +162,7 @@ class RadarrSensor(Entity): self.ssl, self.host, self.port, self.urlbase, start, end), headers={'X-Api-Key': self.apikey}, timeout=10) except OSError: - _LOGGER.error("Host %s is not available", self.host) + _LOGGER.warning("Host %s is not available", self.host) self._available = False self._state = None return diff --git a/homeassistant/components/sensor/raincloud.py b/homeassistant/components/sensor/raincloud.py index d3b8b7207e3..c03aa0a2aec 100644 --- a/homeassistant/components/sensor/raincloud.py +++ b/homeassistant/components/sensor/raincloud.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): RainCloudSensor(raincloud.controller.faucet, sensor_type)) else: - # create an sensor for each zone managed by a faucet + # create a sensor for each zone managed by a faucet for zone in raincloud.controller.faucet.zones: sensors.append(RainCloudSensor(zone, sensor_type)) diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py index 42460a83d6f..090addb5b6e 100644 --- a/homeassistant/components/sensor/sonarr.py +++ b/homeassistant/components/sensor/sonarr.py @@ -181,7 +181,7 @@ class SonarrSensor(Entity): headers={'X-Api-Key': self.apikey}, timeout=10) except OSError: - _LOGGER.error("Host %s is not available", self.host) + _LOGGER.warning("Host %s is not available", self.host) self._available = False self._state = None return diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py new file mode 100644 index 00000000000..99da8c3c680 --- /dev/null +++ b/homeassistant/components/sensor/sql.py @@ -0,0 +1,145 @@ +""" +Sensor from an SQL Query. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sql/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE) +from homeassistant.components.recorder import ( + CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['sqlalchemy==1.2.2'] + +CONF_QUERIES = 'queries' +CONF_QUERY = 'query' +CONF_COLUMN_NAME = 'column' + +_QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): cv.string, + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_QUERIES): [_QUERY_SCHEME], + vol.Optional(CONF_DB_URL): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + db_url = config.get(CONF_DB_URL, None) + if not db_url: + db_url = DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + import sqlalchemy + from sqlalchemy.orm import sessionmaker, scoped_session + + try: + engine = sqlalchemy.create_engine(db_url) + sessionmaker = scoped_session(sessionmaker(bind=engine)) + + # run a dummy query just to test the db_url + sess = sessionmaker() + sess.execute("SELECT 1;") + + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + return + + queries = [] + + for query in config.get(CONF_QUERIES): + name = query.get(CONF_NAME) + query_str = query.get(CONF_QUERY) + unit = query.get(CONF_UNIT_OF_MEASUREMENT) + value_template = query.get(CONF_VALUE_TEMPLATE) + column_name = query.get(CONF_COLUMN_NAME) + + if value_template is not None: + value_template.hass = hass + + sensor = SQLSensor( + name, sessionmaker, query_str, column_name, unit, value_template + ) + queries.append(sensor) + + add_devices(queries, True) + + +class SQLSensor(Entity): + """An SQL sensor.""" + + def __init__(self, name, sessmaker, query, column, unit, value_template): + """Initialize SQL sensor.""" + self._name = name + if "LIMIT" in query: + self._query = query + else: + self._query = query.replace(";", " LIMIT 1;") + self._unit_of_measurement = unit + self._template = value_template + self._column_name = column + self.sessionmaker = sessmaker + self._state = None + self._attributes = None + + @property + def name(self): + """Return the name of the query.""" + return self._name + + @property + def state(self): + """Return the query's current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + def update(self): + """Retrieve sensor data from the query.""" + import sqlalchemy + try: + sess = self.sessionmaker() + result = sess.execute(self._query) + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Error executing query %s: %s", self._query, err) + return + + for res in result: + _LOGGER.debug(res.items()) + data = res[self._column_name] + self._attributes = {k: str(v) for k, v in res.items()} + + if data is None: + _LOGGER.error("%s returned no results", self._query) + return + + if self._template is not None: + self._state = self._template.async_render_with_possible_json_value( + data, None) + else: + self._state = data + + sess.close() diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 19281d36d88..b26fd5cc804 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -34,6 +34,8 @@ ATTR_VARIANCE = 'variance' ATTR_STANDARD_DEVIATION = 'standard_deviation' ATTR_SAMPLING_SIZE = 'sampling_size' ATTR_TOTAL = 'total' +ATTR_MAX_AGE = 'max_age' +ATTR_MIN_AGE = 'min_age' CONF_SAMPLING_SIZE = 'sampling_size' CONF_MAX_AGE = 'max_age' @@ -88,10 +90,11 @@ class StatisticsSensor(Entity): self.median = self.mean = self.variance = self.stdev = 0 self.min = self.max = self.total = self.count = 0 self.average_change = self.change = 0 + self.max_age = self.min_age = 0 if 'recorder' in self._hass.config.components: # only use the database if it's configured - hass.async_add_job(self._initzialize_from_database) + hass.async_add_job(self._initialize_from_database) @callback # pylint: disable=invalid-name @@ -111,8 +114,7 @@ class StatisticsSensor(Entity): try: self.states.append(float(new_state.state)) if self._max_age is not None: - now = dt_util.utcnow() - self.ages.append(now) + self.ages.append(new_state.last_updated) self.count = self.count + 1 except ValueError: self.count = self.count + 1 @@ -141,7 +143,7 @@ class StatisticsSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" if not self.is_binary: - return { + state = { ATTR_MEAN: self.mean, ATTR_COUNT: self.count, ATTR_MAX_VALUE: self.max, @@ -154,6 +156,13 @@ class StatisticsSensor(Entity): ATTR_CHANGE: self.change, ATTR_AVERAGE_CHANGE: self.average_change, } + # Only return min/max age if we have a age span + if self._max_age: + state.update({ + ATTR_MAX_AGE: self.max_age, + ATTR_MIN_AGE: self.min_age, + }) + return state @property def icon(self): @@ -190,6 +199,7 @@ class StatisticsSensor(Entity): self.stdev = self.variance = STATE_UNKNOWN if self.states: + self.count = len(self.states) self.total = round(sum(self.states), 2) self.min = min(self.states) self.max = max(self.states) @@ -197,12 +207,15 @@ class StatisticsSensor(Entity): self.average_change = self.change if len(self.states) > 1: self.average_change /= len(self.states) - 1 + if self._max_age is not None: + self.max_age = max(self.ages) + self.min_age = min(self.ages) else: self.min = self.max = self.total = STATE_UNKNOWN self.average_change = self.change = STATE_UNKNOWN @asyncio.coroutine - def _initzialize_from_database(self): + def _initialize_from_database(self): """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index cfc868de664..f5a41c7b8ce 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -136,7 +136,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SynoApi(object): """Class to interface with Synology DSM API.""" - # pylint: disable=bare-except def __init__(self, host, port, username, password, temp_unit): """Initialize the API wrapper class.""" from SynologyDSM import SynologyDSM @@ -144,7 +143,7 @@ class SynoApi(object): try: self._api = SynologyDSM(host, port, username, password) - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") # Will be updated when update() gets called. diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 57e03cf153f..ea8595e3991 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -24,27 +24,27 @@ CONF_ARG = 'arg' SENSOR_TYPES = { 'disk_free': ['Disk free', 'GiB', 'mdi:harddisk'], - 'disk_use': ['Disk used', 'GiB', 'mdi:harddisk'], - 'disk_use_percent': ['Disk used', '%', 'mdi:harddisk'], + 'disk_use': ['Disk use', 'GiB', 'mdi:harddisk'], + 'disk_use_percent': ['Disk use (percent)', '%', 'mdi:harddisk'], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], 'last_boot': ['Last boot', '', 'mdi:clock'], - 'load_15m': ['Average load (15m)', '', 'mdi:memory'], - 'load_1m': ['Average load (1m)', '', 'mdi:memory'], - 'load_5m': ['Average load (5m)', '', 'mdi:memory'], - 'memory_free': ['RAM available', 'MiB', 'mdi:memory'], - 'memory_use': ['RAM used', 'MiB', 'mdi:memory'], - 'memory_use_percent': ['RAM used', '%', 'mdi:memory'], - 'network_in': ['Received', 'MiB', 'mdi:server-network'], - 'network_out': ['Sent', 'MiB', 'mdi:server-network'], - 'packets_in': ['Packets received', ' ', 'mdi:server-network'], - 'packets_out': ['Packets sent', ' ', 'mdi:server-network'], + 'load_15m': ['Load (15m)', '', 'mdi:memory'], + 'load_1m': ['Load (1m)', '', 'mdi:memory'], + 'load_5m': ['Load (5m)', '', 'mdi:memory'], + 'memory_free': ['Memory free', 'MiB', 'mdi:memory'], + 'memory_use': ['Memory use', 'MiB', 'mdi:memory'], + 'memory_use_percent': ['Memory use (percent)', '%', 'mdi:memory'], + 'network_in': ['Network in', 'MiB', 'mdi:server-network'], + 'network_out': ['Network out', 'MiB', 'mdi:server-network'], + 'packets_in': ['Packets in', ' ', 'mdi:server-network'], + 'packets_out': ['Packets out', ' ', 'mdi:server-network'], 'process': ['Process', ' ', 'mdi:memory'], - 'processor_use': ['CPU used', '%', '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 used', 'GiB', 'mdi:harddisk'], - 'swap_use_percent': ['Swap used', '%', 'mdi:harddisk'], + 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'], + 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 8c7259ff800..7acdc1a20bd 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -147,7 +147,7 @@ class TadoSensor(Entity): data = self._store.get_data(self._data_id) if data is None: - _LOGGER.debug("Recieved no data for zone %s", self.zone_name) + _LOGGER.debug("Received no data for zone %s", self.zone_name) return unit = TEMP_CELSIUS diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index d0b038fd230..39d1cbc75a3 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -9,7 +9,6 @@ import logging from datetime import timedelta from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) @@ -36,7 +35,6 @@ class TahomaSensor(TahomaDevice, Entity): """Initialize the sensor.""" self.current_value = None super().__init__(tahoma_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) @property def state(self): diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 08681fd37f2..55d520cf6ca 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -1,5 +1,5 @@ """ -Support gahtering ted500 information. +Support gathering ted500 information. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ted5000/ diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 1d9bf0b7a9a..b347439e08d 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -31,11 +31,6 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) -SENSOR_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SENSOR_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py index 4534c8d6203..74e74262710 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/sensor/tesla.py @@ -10,7 +10,8 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN from homeassistant.components.tesla import TeslaDevice -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_KILOMETERS, LENGTH_MILES) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -66,18 +67,23 @@ class TeslaSensor(TeslaDevice, Entity): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) self.tesla_device.update() + units = self.tesla_device.measurement + if self.tesla_device.bin_type == 0x4: if self.type == 'outside': self.current_value = self.tesla_device.get_outside_temp() else: self.current_value = self.tesla_device.get_inside_temp() - - tesla_temp_units = self.tesla_device.measurement - - if tesla_temp_units == 'F': + if units == 'F': self._unit = TEMP_FAHRENHEIT else: self._unit = TEMP_CELSIUS else: - self.current_value = self.tesla_device.battery_level() - self._unit = "%" + self.current_value = self.tesla_device.get_value() + if self.tesla_device.bin_type == 0x5: + self._unit = units + elif self.tesla_device.bin_type in (0xA, 0xB): + if units == 'LENGTH_MILES': + self._unit = LENGTH_MILES + else: + self._unit = LENGTH_KILOMETERS diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py index 5f341760bb6..1ca08e7c0aa 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] - # non specificy repository selected, then show all associated + # non specific repository selected, then show all associated if not repositories: all_repos = travis.repos(member=user.login) repositories = [repo.slug for repo in all_repos] diff --git a/homeassistant/components/sensor/uk_transport.py b/homeassistant/components/sensor/uk_transport.py index 9b35afb418c..72d34411d5c 100644 --- a/homeassistant/components/sensor/uk_transport.py +++ b/homeassistant/components/sensor/uk_transport.py @@ -132,7 +132,7 @@ class UkTransportSensor(Entity): _LOGGER.warning('Invalid response from API') elif 'error' in response.json(): if 'exceeded' in response.json()['error']: - self._state = 'Useage limites exceeded' + self._state = 'Usage limits exceeded' if 'invalid' in response.json()['error']: self._state = 'Credentials invalid' else: diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 318a22cfa2a..bf2e263a0bb 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -84,7 +84,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev.append(waqi_sensor) except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): - _LOGGER.exception('Failed to connct to WAQI servers.') + _LOGGER.exception('Failed to connect to WAQI servers.') raise PlatformNotReady async_add_devices(dev, True) diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py index 7d8c71f8d51..24c45ec1ff3 100644 --- a/homeassistant/components/sensor/waterfurnace.py +++ b/homeassistant/components/sensor/waterfurnace.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify class WFSensorConfig(object): """Water Furnace Sensor configuration.""" - def __init__(self, friendly_name, field, icon="mdi:guage", + def __init__(self, friendly_name, field, icon="mdi:gauge", unit_of_measurement=None): """Initialize configuration.""" self.friendly_name = friendly_name diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 8bb449b2ec1..d0d9758c13a 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -55,7 +55,7 @@ class WUSensorConfig(object): https://www.wunderground.com/weather/api/d/docs?d=data/index value (function(WUndergroundData)): callback that extracts desired value from WUndergroundData object - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement entity_picture (string): value or callback returning URL of entity picture icon (string): icon name or URL @@ -84,7 +84,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): dictionary. icon (string): icon name or URL, if None sensor will use current weather symbol - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement """ super().__init__( friendly_name, @@ -230,7 +230,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): value_type (string): "record" or "normal" wu_unit (string): unit name in WU API icon (string): icon name or URL - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement """ super().__init__( friendly_name=friendly_name, diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index f5c20fa5a1c..c2498d88822 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -71,7 +71,7 @@ class XiaomiSensor(XiaomiDevice): value /= 100 elif self._data_key in ['illumination']: value = max(value - 300, 0) - if self._data_key == 'temperature' and (value < -20 or value > 60): + if self._data_key == 'temperature' and (value < -50 or value > 60): return False elif self._data_key == 'humidity' and (value <= 0 or value > 100): return False diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py index 4b63d769243..df5ff5e8d37 100644 --- a/homeassistant/components/sensor/zamg.py +++ b/homeassistant/components/sensor/zamg.py @@ -236,7 +236,7 @@ def closest_station(lat, lon, cache_dir): stations = zamg_stations(cache_dir) def comparable_dist(zamg_id): - """Calculate the psudeo-distance from lat/lon.""" + """Calculate the pseudo-distance from lat/lon.""" station_lat, station_lon = stations[zamg_id] return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index cd2847c1fa6..a1820f7d7dd 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -31,7 +31,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @asyncio.coroutine def make_sensor(discovery_info): """Create ZHA sensors factory.""" - from bellows.zigbee.zcl.clusters.measurement import TemperatureMeasurement + from zigpy.zcl.clusters.measurement import TemperatureMeasurement in_clusters = discovery_info['in_clusters'] if TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 8ec023057d1..31259325c04 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -1,4 +1,4 @@ -"""Component to manage a shoppling list.""" +"""Component to manage a shopping list.""" import asyncio import json import logging diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 5c35e43881e..d085b1279cb 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -155,7 +155,7 @@ def async_setup(hass, config): def resolve_slot_values(slot): - """Convert snips builtin types to useable values.""" + """Convert snips builtin types to usable values.""" if 'value' in slot['value']: value = slot['value']['value'] else: diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 141d06768e3..72477a5a65f 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -151,7 +151,7 @@ def _ws_process_message(message, async_callback, *args): "Unsuccessful websocket message delivered, ignoring: %s", message) try: yield from async_callback(message['data']['sia'], *args) - except: # pylint: disable=bare-except + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception("Exception in callback, ignoring") diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index 58361b2e8b2..8fd70ec7ed8 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class AcerSwitch(SwitchDevice): - """Represents an Acer Projector as an switch.""" + """Represents an Acer Projector as a switch.""" def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" @@ -103,13 +103,13 @@ class AcerSwitch(SwitchDevice): # need to wait for timeout ret = self.ser.read_until(size=20).decode('utf-8') except serial.SerialException: - _LOGGER.error('Problem comunicating with %s', self._serial_port) + _LOGGER.error('Problem communicating with %s', self._serial_port) self.ser.close() return ret def _write_read_format(self, msg): - """Write msg, obtain awnser and format output.""" - # awnsers are formatted as ***\rawnser\r*** + """Write msg, obtain answer and format output.""" + # answers are formatted as ***\answer\r*** awns = self._write_read(msg) match = re.search(r'\r(.+)\r', awns) if match: diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 5841642cc00..e79b7c3f34c 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -100,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): packet = yield from hass.async_add_job( broadlink_device.check_data) if packet: - log_msg = "Recieved packet is: {}".\ + log_msg = "Received packet is: {}".\ format(b64encode(packet).decode('utf8')) _LOGGER.info(log_msg) hass.components.persistent_notification.async_create( @@ -144,7 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, SERVICE_LEARN + '_' + ip_addr.replace('.', '_'), _learn_command) hass.services.register(DOMAIN, SERVICE_SEND + '_' + - ip_addr.replace('.', '_'), _send_packet) + ip_addr.replace('.', '_'), _send_packet, + vol.Schema({'packet': cv.ensure_list})) switches = [] for object_id, device_config in devices.items(): switches.append( diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py index 0625a42f765..f3af70c6222 100644 --- a/homeassistant/components/switch/digitalloggers.py +++ b/homeassistant/components/switch/digitalloggers.py @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DINRelay(SwitchDevice): - """Representation of a individual DIN III relay port.""" + """Representation of an individual DIN III relay port.""" def __init__(self, controller_name, parent_device, outlet): """Initialize the DIN III Relay switch.""" diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index ff432f2efc8..acc0c3ac423 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -33,7 +33,7 @@ CONF_START_CT = 'start_colortemp' CONF_SUNSET_CT = 'sunset_colortemp' CONF_STOP_CT = 'stop_colortemp' CONF_BRIGHTNESS = 'brightness' -CONF_DISABLE_BRIGTNESS_ADJUST = 'disable_brightness_adjust' +CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' CONF_INTERVAL = 'interval' MODE_XY = 'xy' @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_LIGHTS): cv.entity_ids, vol.Optional(CONF_NAME, default="Flux"): cv.string, vol.Optional(CONF_START_TIME): cv.time, - vol.Optional(CONF_STOP_TIME, default=datetime.time(22, 0)): cv.time, + vol.Optional(CONF_STOP_TIME): cv.time, vol.Optional(CONF_START_CT, default=4000): vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), vol.Optional(CONF_SUNSET_CT, default=3000): @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), vol.Optional(CONF_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - vol.Optional(CONF_DISABLE_BRIGTNESS_ADJUST): cv.boolean, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Any(MODE_XY, MODE_MIRED, MODE_RGB), vol.Optional(CONF_INTERVAL, default=30): cv.positive_int, @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sunset_colortemp = config.get(CONF_SUNSET_CT) stop_colortemp = config.get(CONF_STOP_CT) brightness = config.get(CONF_BRIGHTNESS) - disable_brightness_adjust = config.get(CONF_DISABLE_BRIGTNESS_ADJUST) + disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST) mode = config.get(CONF_MODE) interval = config.get(CONF_INTERVAL) transition = config.get(ATTR_TRANSITION) @@ -184,9 +184,7 @@ class FluxSwitch(SwitchDevice): sunset = get_astral_event_date(self.hass, 'sunset', now.date()) start_time = self.find_start_time(now) - stop_time = now.replace( - hour=self._stop_time.hour, minute=self._stop_time.minute, - second=0) + stop_time = self.find_stop_time(now) if stop_time <= start_time: # stop_time does not happen in the same day as start_time @@ -210,7 +208,7 @@ class FluxSwitch(SwitchDevice): else: temp = self._start_colortemp + temp_offset else: - # Nightime + # Night time time_state = 'night' if now < stop_time: @@ -270,3 +268,13 @@ class FluxSwitch(SwitchDevice): else: sunrise = get_astral_event_date(self.hass, 'sunrise', now.date()) return sunrise + + def find_stop_time(self, now): + """Return dusk or stop_time if given.""" + if self._stop_time: + dusk = now.replace( + hour=self._stop_time.hour, minute=self._stop_time.minute, + second=0) + else: + dusk = get_astral_event_date(self.hass, 'dusk', now.date()) + return dusk diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py index a100b582e64..65a7a762c0f 100644 --- a/homeassistant/components/switch/hdmi_cec.py +++ b/homeassistant/components/switch/hdmi_cec.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CecSwitchDevice(CecDevice, SwitchDevice): """Representation of a HDMI device as a Switch.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, hass, device, logical) self.entity_id = "%s.%s_%s" % ( diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py index 97d4320280d..67ebe95ba8e 100644 --- a/homeassistant/components/switch/hive.py +++ b/homeassistant/components/switch/hive.py @@ -65,5 +65,5 @@ class HiveDevicePlug(SwitchDevice): entity.handle_update(self.data_updatesource) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/switch/ihc.py index 4bab1378acd..eab88035c73 100644 --- a/homeassistant/components/switch/ihc.py +++ b/homeassistant/components/switch/ihc.py @@ -53,7 +53,7 @@ class IHCSwitch(IHCDevice, SwitchDevice): """IHC Switch.""" def __init__(self, ihc_controller, name: str, ihc_id: int, - info: bool, product: Element=None): + info: bool, product: Element=None) -> None: """Initialize the IHC switch.""" super().__init__(ihc_controller, name, ihc_id, product) self._state = False diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index c20a638c00f..4456436ea61 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -54,7 +54,7 @@ class InsteonLocalSwitchDevice(SwitchDevice): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index ed7d0ffc479..0b584e14b8d 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -83,7 +83,7 @@ class InsteonPLMSwitchDevice(SwitchDevice): @callback def async_switch_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info('Received update calback from PLM for %s', self._address) + _LOGGER.info('Received update callback from PLM for %s', self._address) self._hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index e0b656aafe9..01c08767ca0 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -51,7 +51,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up switch for KNX platform configured within plattform.""" + """Set up switch for KNX platform configured within platform.""" import xknx switch = xknx.devices.Switch( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 62bc5f99d01..a797abb47fc 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -1,5 +1,5 @@ """ -Support for Neato Connected Vaccums switches. +Support for Neato Connected Vacuums switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.neato/ @@ -68,7 +68,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF - _LOGGER.debug("Shedule state: %s", self._schedule_state) + _LOGGER.debug("Schedule state: %s", self._schedule_state) @property def name(self): diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 201aee0f58c..1ce599366a1 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -19,9 +19,9 @@ from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) CONF_OFF_CODE = 'off_code' -CONF_OFF_CODE_RECIEVE = 'off_code_receive' +CONF_OFF_CODE_RECEIVE = 'off_code_receive' CONF_ON_CODE = 'on_code' -CONF_ON_CODE_RECIEVE = 'on_code_receive' +CONF_ON_CODE_RECEIVE = 'on_code_receive' CONF_SYSTEMCODE = 'systemcode' CONF_UNIT = 'unit' CONF_UNITCODE = 'unitcode' @@ -48,9 +48,9 @@ SWITCHES_SCHEMA = vol.Schema({ vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + vol.Optional(CONF_OFF_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), - vol.Optional(CONF_ON_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + vol.Optional(CONF_ON_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list, [COMMAND_SCHEMA]) }) @@ -72,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): properties.get(CONF_NAME, dev_name), properties.get(CONF_ON_CODE), properties.get(CONF_OFF_CODE), - properties.get(CONF_ON_CODE_RECIEVE), - properties.get(CONF_OFF_CODE_RECIEVE) + properties.get(CONF_ON_CODE_RECEIVE), + properties.get(CONF_OFF_CODE_RECEIVE) ) ) diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index 03f9e84b3c8..007e74e14fd 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -131,7 +131,7 @@ class PAServer(): self._send_command(str.format(UNLOAD_CMD, module_idx), False) def get_module_idx(self, sink_name, source_name): - """For a sink/source, return it's module id in our cache, if found.""" + """For a sink/source, return its module id in our cache, if found.""" result = re.search(str.format(MOD_REGEX, re.escape(sink_name), re.escape(source_name)), self._current_module_state) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index a1ce83b597a..d8d424be361 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -134,7 +134,7 @@ class RachioIro(object): return self._running def list_zones(self, include_disabled=False): - """Return alist of the zones connected to the device, incl. data.""" + """Return a list of the zones connected to the device, incl. data.""" if not self._zones: self._zones = [RachioZone(self.rachio, self, zone['id'], self.manual_run_mins) diff --git a/homeassistant/components/switch/rainbird.py b/homeassistant/components/switch/rainbird.py index ee283b3c269..9aa24b9360b 100644 --- a/homeassistant/components/switch/rainbird.py +++ b/homeassistant/components/switch/rainbird.py @@ -52,7 +52,7 @@ class RainBirdSwitch(SwitchDevice): self._devid = dev_id self._zone = int(dev.get(CONF_ZONE)) self._name = dev.get(CONF_FRIENDLY_NAME, - "Sprinker {}".format(self._zone)) + "Sprinkler {}".format(self._zone)) self._state = None self._duration = dev.get(CONF_TRIGGER_TIME) self._attributes = { diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py index f373a6aad84..a18d6544acc 100644 --- a/homeassistant/components/switch/raincloud.py +++ b/homeassistant/components/switch/raincloud.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - # create an sensor for each zone managed by faucet + # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append( RainCloudSwitch(default_watering_timer, diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 9425b61f0e5..3147ded96bd 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -180,8 +180,7 @@ class RainMachineEntity(SwitchDevice): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return '{}.{}.{}'.format(self.__class__, self._device_name, - self.rainmachine_id) + return self.rainmachine_id @aware_throttle('local') def _local_update(self) -> None: diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index dfcf1816b7b..8b2734612de 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -155,7 +155,7 @@ class SCSGateSwitch(SwitchDevice): class SCSGateScenarioSwitch(object): """Provides a SCSGate scenario switch. - This switch is always in a 'off" state, when toggled it's used to trigger + This switch is always in an 'off" state, when toggled it's used to trigger events. """ diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 64dafdcadef..93ebf98e9ac 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -38,11 +38,6 @@ SWITCH_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) -SWITCH_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SWITCH_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), }) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index f67aaec9796..14faa98fb59 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -50,8 +50,7 @@ class SmartPlugSwitch(SwitchDevice): """Initialize the switch.""" self.smartplug = smartplug self._name = name - if leds_on is not None: - self.smartplug.led = leds_on + self._leds_on = leds_on self._state = None self._available = True # Set up emeter cache @@ -94,6 +93,10 @@ class SmartPlugSwitch(SwitchDevice): self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON + if self._leds_on is not None: + self.smartplug.led = self._leds_on + self._leds_on = None + # Pull the name from the device if a name was not specified if self._name == DEFAULT_NAME: self._name = self.smartplug.alias diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index d94ff8c268b..ecaff14e2e2 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from homeassistant.const import (CONF_HOST, CONF_NAME) -REQUIREMENTS = ['wakeonlan==0.2.2'] +REQUIREMENTS = ['wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class WOLSwitch(SwitchDevice): def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): """Initialize the WOL switch.""" - from wakeonlan import wol + import wakeonlan self._hass = hass self._name = name self._host = host @@ -61,7 +61,7 @@ class WOLSwitch(SwitchDevice): self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False - self._wol = wol + self._wol = wakeonlan @property def should_poll(self): diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 7b97ece337b..4339c92bb60 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -81,7 +81,7 @@ class WemoSwitch(SwitchDevice): @property def unique_id(self): """Return the ID of this WeMo switch.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index f2fdf3177aa..87871079a9c 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 5c8fe3109a6..1dad1f3a1eb 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -16,6 +16,7 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' CONF_MESSAGE = 'message' @@ -27,6 +28,8 @@ DEFAULT_MAX_ENTRIES = 50 DEPENDENCIES = ['http'] DOMAIN = 'system_log' +EVENT_SYSTEM_LOG = 'system_log_event' + SERVICE_CLEAR = 'clear' SERVICE_WRITE = 'write' @@ -46,67 +49,6 @@ SERVICE_WRITE_SCHEMA = vol.Schema({ }) -class LogErrorHandler(logging.Handler): - """Log handler for error messages.""" - - def __init__(self, maxlen): - """Initialize a new LogErrorHandler.""" - super().__init__() - self.records = deque(maxlen=maxlen) - - def emit(self, record): - """Save error and warning logs. - - Everything logged with error or warning is saved in local buffer. A - default upper limit is set to 50 (older entries are discarded) but can - be changed if needed. - """ - if record.levelno >= logging.WARN: - stack = [] - if not record.exc_info: - try: - stack = [f for f, _, _, _ in traceback.extract_stack()] - except ValueError: - # On Python 3.4 under py.test getting the stack might fail. - pass - self.records.appendleft([record, stack]) - - -@asyncio.coroutine -def async_setup(hass, config): - """Set up the logger component.""" - conf = config.get(DOMAIN) - - if conf is None: - conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - - handler = LogErrorHandler(conf.get(CONF_MAX_ENTRIES)) - logging.getLogger().addHandler(handler) - - hass.http.register_view(AllErrorsView(handler)) - - @asyncio.coroutine - def async_service_handler(service): - """Handle logger services.""" - if service.service == 'clear': - handler.records.clear() - return - if service.service == 'write': - logger = logging.getLogger( - service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) - level = service.data[CONF_LEVEL] - getattr(logger, level)(service.data[CONF_MESSAGE]) - - hass.services.async_register( - DOMAIN, SERVICE_CLEAR, async_service_handler, - schema=SERVICE_CLEAR_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_WRITE, async_service_handler, - schema=SERVICE_WRITE_SCHEMA) - - return True - - def _figure_out_source(record, call_stack, hass): paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] try: @@ -134,10 +76,11 @@ def _figure_out_source(record, call_stack, hass): # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. + paths_re = r'(?:{})/(.*)'.format('|'.join([re.escape(x) for x in paths])) for pathname in reversed(stack): # Try to match with a file within Home Assistant - match = re.match(r'(?:{})/(.*)'.format('|'.join(paths)), pathname) + match = re.match(paths_re, pathname) if match: return match.group(1) # Ok, we don't know what this is @@ -151,14 +94,86 @@ def _exception_as_string(exc_info): return buf.getvalue() -def _convert(record, call_stack, hass): - return { - 'timestamp': record.created, - 'level': record.levelname, - 'message': record.getMessage(), - 'exception': _exception_as_string(record.exc_info), - 'source': _figure_out_source(record, call_stack, hass), - } +class LogErrorHandler(logging.Handler): + """Log handler for error messages.""" + + def __init__(self, hass, maxlen): + """Initialize a new LogErrorHandler.""" + super().__init__() + self.hass = hass + self.records = deque(maxlen=maxlen) + + def _create_entry(self, record, call_stack): + return { + 'timestamp': record.created, + 'level': record.levelname, + 'message': record.getMessage(), + 'exception': _exception_as_string(record.exc_info), + 'source': _figure_out_source(record, call_stack, self.hass), + } + + def emit(self, record): + """Save error and warning logs. + + Everything logged with error or warning is saved in local buffer. A + default upper limit is set to 50 (older entries are discarded) but can + be changed if needed. + """ + if record.levelno >= logging.WARN: + stack = [] + if not record.exc_info: + try: + stack = [f for f, _, _, _ in traceback.extract_stack()] + except ValueError: + # On Python 3.4 under py.test getting the stack might fail. + pass + + entry = self._create_entry(record, stack) + self.records.appendleft(entry) + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the logger component.""" + conf = config.get(DOMAIN) + if conf is None: + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + + handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + logging.getLogger().addHandler(handler) + + hass.http.register_view(AllErrorsView(handler)) + + @asyncio.coroutine + def async_service_handler(service): + """Handle logger services.""" + if service.service == 'clear': + handler.records.clear() + return + if service.service == 'write': + logger = logging.getLogger( + service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) + level = service.data[CONF_LEVEL] + getattr(logger, level)(service.data[CONF_MESSAGE]) + + @asyncio.coroutine + def async_shutdown_handler(event): + """Remove logging handler when Home Assistant is shutdown.""" + # This is needed as older logger instances will remain + logging.getLogger().removeHandler(handler) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + async_shutdown_handler) + + hass.services.async_register( + DOMAIN, SERVICE_CLEAR, async_service_handler, + schema=SERVICE_CLEAR_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_WRITE, async_service_handler, + schema=SERVICE_WRITE_SCHEMA) + + return True class AllErrorsView(HomeAssistantView): @@ -174,5 +189,6 @@ class AllErrorsView(HomeAssistantView): @asyncio.coroutine def get(self, request): """Get all errors and warnings.""" - return self.json([_convert(x[0], x[1], request.app['hass']) - for x in self.handler.records]) + # deque is not serializable (it's just "list-like") so it must be + # converted to a list before it can be serialized to json + return self.json(list(self.handler.records)) diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py index 1f5125d724e..cfba0a5c0c4 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado.py @@ -119,8 +119,10 @@ class TadoDataStore: def reset_zone_overlay(self, zone_id): """Wrap for resetZoneOverlay(..).""" - return self.tado.resetZoneOverlay(zone_id) + self.tado.resetZoneOverlay(zone_id) + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None): """Wrap for setZoneOverlay(..).""" - return self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 4eacee08ec1..28a54f40d56 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -13,9 +13,8 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import (slugify) -REQUIREMENTS = ['tahoma-api==0.0.10'] +REQUIREMENTS = ['tahoma-api==0.0.11'] _LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def setup(hass, config): api.get_setup() devices = api.get_devices() except RequestException: - _LOGGER.exception("Cannot fetch informations from Tahoma API") + _LOGGER.exception("Cannot fetch information from Tahoma API") return False hass.data[DOMAIN] = { @@ -101,15 +100,8 @@ class TahomaDevice(Entity): """Initialize the device.""" self.tahoma_device = tahoma_device self.controller = controller - self._unique_id = TAHOMA_ID_FORMAT.format( - slugify(tahoma_device.label), slugify(tahoma_device.url)) self._name = self.tahoma_device.label - @property - def unique_id(self): - """Return the unique ID for this cover.""" - return self._unique_id - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index cb314c4a2b4..170e1517a6d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -330,7 +330,7 @@ class TelegramNotificationService: This can be one of (message_id, inline_message_id) from a msg dict, returning a tuple. **You can use 'last' as message_id** to edit - the last sended message in the chat_id. + the message last sent in the chat_id. """ message_id = inline_message_id = None if ATTR_MESSAGEID in msg_data: @@ -354,7 +354,7 @@ class TelegramNotificationService: chat_ids = [t for t in target if t in self.allowed_chat_ids] if chat_ids: return chat_ids - _LOGGER.warning("Unallowed targets: %s, using default: %s", + _LOGGER.warning("Disallowed targets: %s, using default: %s", target, self._default_user) return [self._default_user] diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index dc864c9f61a..4c144fe42db 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -25,7 +25,7 @@ send_message: 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 asociated callback data. + 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"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' send_photo: @@ -56,7 +56,7 @@ send_photo: 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 asociated callback data. + 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: @@ -87,7 +87,7 @@ send_video: 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 asociated callback data. + 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_document: @@ -118,7 +118,7 @@ send_document: 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 asociated callback data. + 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_location: @@ -140,7 +140,7 @@ send_location: 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 asociated callback data. + 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"]]]' edit_message: @@ -165,7 +165,7 @@ edit_message: description: Disables link previews for links in the message. example: true inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + 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"]]]' edit_caption: @@ -181,7 +181,7 @@ edit_caption: description: Message body of the notification. example: The garage door has been open for 10 minutes. inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + 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"]]]' edit_replymarkup: @@ -194,7 +194,7 @@ edit_replymarkup: description: The chat_id where to edit the reply_markup. example: 12345 inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + 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"]]]' answer_callback_query: diff --git a/homeassistant/components/tesla.py b/homeassistant/components/tesla.py index 4f76b70432e..76b5c00d9d4 100644 --- a/homeassistant/components/tesla.py +++ b/homeassistant/components/tesla.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -REQUIREMENTS = ['teslajsonpy==0.0.19'] +REQUIREMENTS = ['teslajsonpy==0.0.23'] DOMAIN = 'tesla' diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d85b7d189c5..532b4529eca 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['mutagen==1.39'] +REQUIREMENTS = ['mutagen==1.40.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 85b223864e9..084a7229212 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -87,7 +87,7 @@ class GoogleProvider(Provider): url_param = { 'ie': 'UTF-8', 'tl': language, - 'q': yarl.quote(part), + 'q': yarl.URL(part).raw_path, 'tk': part_token, 'total': len(message_parts), 'idx': idx, diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py index 0c4eba54e35..58f858b0975 100644 --- a/homeassistant/components/usps.py +++ b/homeassistant/components/usps.py @@ -69,7 +69,7 @@ class USPSData(object): """ def __init__(self, session, name): - """Initialize the data oject.""" + """Initialize the data object.""" self.session = session self.name = name self.packages = [] diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 29099db5cd5..2a4eb2d5e7f 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -1,5 +1,5 @@ """ -Support for Neato Connected Vaccums. +Support for Neato Connected Vacuums. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 06690a0909f..d64f7a754ee 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,12 +19,12 @@ 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.4'] +REQUIREMENTS = ['python-miio==0.3.5'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Vacuum cleaner' -ICON = 'mdi:google-circles-group' +ICON = 'mdi:roomba' PLATFORM = 'xiaomi_miio' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index dcd4ed518d0..3e36d0a3028 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -93,7 +93,7 @@ def setup(hass, config): hass, component, DOMAIN, (vehicle.vin, attr), config) def update_vehicle(vehicle): - """Revieve updated information on vehicle.""" + """Receive updated information on vehicle.""" state.vehicles[vehicle.vin] = vehicle if vehicle.vin not in state.entities: discover_vehicle(vehicle) diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py index 7da0f3054f3..4e729c7ccc7 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['wakeonlan==0.2.2'] +REQUIREMENTS = ['wakeonlan==1.0.0'] DOMAIN = "wake_on_lan" _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the wake on LAN component.""" - from wakeonlan import wol + import wakeonlan @asyncio.coroutine def send_magic_packet(call): @@ -42,11 +42,11 @@ def async_setup(hass, config): mac_address, broadcast_address) if broadcast_address is not None: yield from hass.async_add_job( - partial(wol.send_magic_packet, mac_address, + partial(wakeonlan.send_magic_packet, mac_address, ip_address=broadcast_address)) else: yield from hass.async_add_job( - partial(wol.send_magic_packet, mac_address)) + partial(wakeonlan.send_magic_packet, mac_address)) hass.services.async_register( DOMAIN, SERVICE_SEND_MAGIC_PACKET, send_magic_packet, diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace.py index 346bdcdfb97..a587285e0ba 100644 --- a/homeassistant/components/waterfurnace.py +++ b/homeassistant/components/waterfurnace.py @@ -18,7 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery -REQUIREMENTS = ["waterfurnace==0.3.0"] +REQUIREMENTS = ["waterfurnace==0.4.0"] _LOGGER = logging.getLogger(__name__) @@ -83,6 +83,8 @@ class WaterFurnaceData(threading.Thread): def run(self): """Thread run loop.""" + import waterfurnace.waterfurnace as wf + @callback def register(): """Connect to hass for shutdown.""" @@ -110,8 +112,11 @@ class WaterFurnaceData(threading.Thread): try: self.data = self.client.read() - except ConnectionError: - # attempt to log back in if there was a session expiration. + except wf.WFException: + # WFExceptions are things the WF library understands + # that pretty much can all be solved by logging in and + # back out again. + _LOGGER.exception("Failed to read data, attempting to recover") try: self.client.login() except Exception: # pylint: disable=broad-except @@ -127,10 +132,6 @@ class WaterFurnaceData(threading.Thread): "Lost our connection to websocket, trying again") time.sleep(SCAN_INTERVAL.seconds) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error updating waterfurnace data.") - time.sleep(SCAN_INTERVAL.seconds) - else: self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) time.sleep(SCAN_INTERVAL.seconds) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 0566cc03662..21f67ce080a 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -1,5 +1,5 @@ """ -Patform for retrieving meteorological data from Dark Sky. +Platform for retrieving meteorological data from Dark Sky. For more details about this platform, please refer to the documentation https://home-assistant.io/components/weather.darksky/ diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 1ff5eeaa535..c8a1bdf8f68 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -21,12 +21,14 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) +ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = 'Data provided by OpenWeatherMap' DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3 CONDITION_CLASSES = { 'cloudy': [804], @@ -137,10 +139,18 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return [{ - ATTR_FORECAST_TIME: entry.get_reference_time('iso'), - ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp')} - for entry in self.forecast_data.get_weathers()] + data = [] + for entry in self.forecast_data.get_weathers(): + data.append({ + ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, + ATTR_FORECAST_TEMP: + entry.get_temperature('celsius').get('temp') + }) + if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0: + data[len(data) - 1][ATTR_FORECAST_CONDITION] = \ + [k for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v][0] + return data def update(self): """Get the latest data from OWM and updates the states.""" @@ -180,7 +190,7 @@ class WeatherData(object): @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) def update_forecast(self): - """Get the lastest forecast from OpenWeatherMap.""" + """Get the latest forecast from OpenWeatherMap.""" from pyowm.exceptions.api_call_error import APICallError try: diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink.py index f55fe1f0bb5..a20b0fc9b0c 100644 --- a/homeassistant/components/weblink.py +++ b/homeassistant/components/weblink.py @@ -16,12 +16,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_ENTITIES = 'entities' +CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." +CONF_RELATIVE_URL_REGEX = r'\A/' DOMAIN = 'weblink' ENTITIES_SCHEMA = vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Required(CONF_URL): vol.Url(), + vol.Required(CONF_URL): vol.Any( + vol.Match(CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG), + cv.url), vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, }) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index a4bfc46bf83..030d1bee579 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/developers/websocket_api/ """ import asyncio +from concurrent import futures from contextlib import suppress from functools import partial import json @@ -120,6 +121,11 @@ BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ TYPE_PING) }, extra=vol.ALLOW_EXTRA) +# Define the possible errors that occur when connections are cancelled. +# Originally, this was just asyncio.CancelledError, but issue #9546 showed +# that futures.CancelledErrors can also occur in some situations. +CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) + def auth_ok_message(): """Return an auth_ok message.""" @@ -231,7 +237,7 @@ class ActiveConnection: def _writer(self): """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, asyncio.CancelledError): + with suppress(RuntimeError, *CANCELLATION_ERRORS): while not self.wsock.closed: message = yield from self.to_write.get() if message is None: @@ -363,7 +369,7 @@ class ActiveConnection: self.log_error(msg) self._writer_task.cancel() - except asyncio.CancelledError: + except CANCELLATION_ERRORS: self.debug("Connection cancelled by server") except asyncio.QueueFull: diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index f87537a1938..58d44b31994 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +import enum import logging import voluptuous as vol @@ -14,13 +15,26 @@ from homeassistant import const as ha_const from homeassistant.helpers import discovery, entity from homeassistant.util import slugify -REQUIREMENTS = ['bellows==0.4.0'] +REQUIREMENTS = [ + 'bellows==0.5.0', + 'zigpy==0.0.1', + 'zigpy-xbee==0.0.1', +] DOMAIN = 'zha' + +class RadioType(enum.Enum): + """Possible options for radio type in config.""" + + ezsp = 'ezsp' + xbee = 'xbee' + + CONF_BAUDRATE = 'baudrate' CONF_DATABASE = 'database_path' CONF_DEVICE_CONFIG = 'device_config' +CONF_RADIO_TYPE = 'radio_type' CONF_USB_PATH = 'usb_path' DATA_DEVICE_CONFIG = 'zha_device_config' @@ -30,6 +44,8 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ + vol.Optional(CONF_RADIO_TYPE, default=RadioType.ezsp): + cv.enum(RadioType), CONF_USB_PATH: cv.string, vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int, CONF_DATABASE: cv.string, @@ -67,16 +83,22 @@ def async_setup(hass, config): """ global APPLICATION_CONTROLLER - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - - ezsp_ = bellows.ezsp.EZSP() usb_path = config[DOMAIN].get(CONF_USB_PATH) baudrate = config[DOMAIN].get(CONF_BAUDRATE) - yield from ezsp_.connect(usb_path, baudrate) + radio_type = config[DOMAIN].get(CONF_RADIO_TYPE) + if radio_type == RadioType.ezsp: + import bellows.ezsp + from bellows.zigbee.application import ControllerApplication + radio = bellows.ezsp.EZSP() + elif radio_type == RadioType.xbee: + import zigpy_xbee.api + from zigpy_xbee.zigbee.application import ControllerApplication + radio = zigpy_xbee.api.XBee() + + yield from radio.connect(usb_path, baudrate) database = config[DOMAIN].get(CONF_DATABASE) - APPLICATION_CONTROLLER = ControllerApplication(ezsp_, database) + APPLICATION_CONTROLLER = ControllerApplication(radio, database) listener = ApplicationListener(hass, config) APPLICATION_CONTROLLER.add_listener(listener) yield from APPLICATION_CONTROLLER.startup(auto_form=True) @@ -130,7 +152,7 @@ class ApplicationListener: @asyncio.coroutine def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" - import bellows.zigbee.profiles + import zigpy.profiles import homeassistant.components.zha.const as zha_const zha_const.populate_data() @@ -146,8 +168,8 @@ class ApplicationListener: node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get( device_key, {}) - if endpoint.profile_id in bellows.zigbee.profiles.PROFILES: - profile = bellows.zigbee.profiles.PROFILES[endpoint.profile_id] + if endpoint.profile_id in zigpy.profiles.PROFILES: + profile = zigpy.profiles.PROFILES[endpoint.profile_id] if zha_const.DEVICE_CLASS.get(endpoint.profile_id, {}).get(endpoint.device_type, None): @@ -253,7 +275,7 @@ class Entity(entity.Entity): """Handle an attribute updated on this cluster.""" pass - def zdo_command(self, aps_frame, tsn, command_id, args): + def zdo_command(self, tsn, command_id, args): """Handle a ZDO command received on this cluster.""" pass diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index b1659536e32..a8d4671ebf7 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -11,8 +11,8 @@ def populate_data(): These cannot be module level, as importing bellows must be done in a in a function. """ - from bellows.zigbee import zcl - from bellows.zigbee.profiles import PROFILES, zha, zll + from zigpy import zcl + from zigpy.profiles import PROFILES, zha, zll DEVICE_CLASS[zha.PROFILE_ID] = { zha.DeviceType.ON_OFF_SWITCH: 'switch', diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 9b80581b85e..0149bb9287a 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -33,7 +33,7 @@ from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS from .util import check_node_schema, check_value_schema, node_name -REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.0.35'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] _LOGGER = logging.getLogger(__name__) @@ -170,7 +170,7 @@ def _obj_to_dict(obj): """Convert an object into a hash for debug.""" return {key: getattr(obj, key) for key in dir(obj) - if key[0] != '_' and not hasattr(getattr(obj, key), '__call__')} + if key[0] != '_' and not callable(getattr(obj, key))} def _value_name(value): @@ -219,7 +219,7 @@ def get_config_value(node, value_index, tries=5): and value.index == value_index): return value.data except RuntimeError: - # If we get an runtime error the dict has changed while + # If we get a runtime error the dict has changed while # we was looking for a value, just do it again return None if tries <= 0 else get_config_value( node, value_index, tries=tries - 1) @@ -865,8 +865,8 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "ZWAVE-{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = "{}-{}".format(self.node.node_id, + self.values.primary.object_id) self._update_attributes() dispatcher.connect( diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 2815be45df2..e2524aefadf 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -182,8 +182,8 @@ SPECIFIC_TYPE_NOT_USED = 0 # Available in all Generic types GENERIC_TYPE_AV_CONTROL_POINT = 3 SPECIFIC_TYPE_DOORBELL = 18 -SPECIFIC_TYPE_SATELLITE_RECIEVER = 4 -SPECIFIC_TYPE_SATELLITE_RECIEVER_V2 = 17 +SPECIFIC_TYPE_SATELLITE_RECEIVER = 4 +SPECIFIC_TYPE_SATELLITE_RECEIVER_V2 = 17 GENERIC_TYPE_DISPLAY = 4 SPECIFIC_TYPE_SIMPLE_DISPLAY = 1 diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index ba8e177c9f7..61855143d59 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -4,7 +4,7 @@ change_association: description: Change an association in the Z-Wave network. fields: association: - description: Specify add or remove assosication + description: Specify add or remove association example: add node_id: description: Node id of the node to set association for. @@ -30,14 +30,14 @@ heal_network: description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress. fields: return_routes: - description: Wheter or not to update the return routes from the nodes to the controller. Defaults to False. + description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. example: True heal_node: description: Start a Z-Wave node heal. Refer to OZW.log for progress. fields: return_routes: - description: Wheter or not to update the return routes from the node to the controller. Defaults to False. + description: Whether or not to update the return routes from the node to the controller. Defaults to False. example: True remove_node: diff --git a/homeassistant/config.py b/homeassistant/config.py index 3f4c4c174d7..5e82ef1baa0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -95,6 +95,10 @@ conversation: # Enables support for tracking state changes over time history: +# Tracked history is kept for 10 days +recorder: + purge_keep_days: 10 + # View all events in a logbook logbook: diff --git a/homeassistant/const.py b/homeassistant/const.py index 6470f50d460..1c923a35936 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,13 +1,12 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 62 -PATCH_VERSION = '1' +MINOR_VERSION = 63 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) REQUIRED_PYTHON_VER_WIN = (3, 5, 2) -CONSTRAINT_FILE = 'package_constraints.txt' # Format for platforms PLATFORM_FORMAT = '{}.{}' diff --git a/homeassistant/core.py b/homeassistant/core.py index 18cf40d3854..b1cf9c51efd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -66,7 +66,7 @@ def valid_entity_id(entity_id: str) -> bool: def valid_state(state: str) -> bool: - """Test if an state is valid.""" + """Test if a state is valid.""" return len(state) < 256 @@ -777,7 +777,7 @@ class ServiceCall(object): self.call_id = call_id def __repr__(self): - """Return the represenation of the service.""" + """Return the representation of the service.""" if self.data: return "".format( self.domain, self.service, util.repr_helper(self.data)) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index ee4176a8937..73a09464439 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -8,7 +8,7 @@ def deprecated_substitute(substitute_name): When a property is added to replace an older property, this decorator can be added to the new property, listing the old property as the substitute. - If the old property is defined, it's value will be used instead, and a log + If the old property is defined, its value will be used instead, and a log warning will be issued alerting the user of the impending change. """ def decorator(func): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e3816fdaa6f..c7653d5d5b9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -77,7 +77,7 @@ class Entity(object): # Protect for multiple updates _update_staged = False - # Process updates pararell + # Process updates in parallel parallel_updates = None @property @@ -91,7 +91,7 @@ class Entity(object): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}.{}".format(self.__class__, id(self)) + return None @property def name(self) -> Optional[str]: @@ -152,7 +152,7 @@ class Entity(object): @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" - return None + return False @property def force_update(self) -> bool: @@ -221,21 +221,41 @@ class Entity(object): if device_attr is not None: attr.update(device_attr) - self._attr_setter('unit_of_measurement', str, ATTR_UNIT_OF_MEASUREMENT, - attr) + unit_of_measurement = self.unit_of_measurement + if unit_of_measurement is not None: + attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement - self._attr_setter('name', str, ATTR_FRIENDLY_NAME, attr) - self._attr_setter('icon', str, ATTR_ICON, attr) - self._attr_setter('entity_picture', str, ATTR_ENTITY_PICTURE, attr) - self._attr_setter('hidden', bool, ATTR_HIDDEN, attr) - self._attr_setter('assumed_state', bool, ATTR_ASSUMED_STATE, attr) - self._attr_setter('supported_features', int, ATTR_SUPPORTED_FEATURES, - attr) - self._attr_setter('device_class', str, ATTR_DEVICE_CLASS, attr) + name = self.name + if name is not None: + attr[ATTR_FRIENDLY_NAME] = name + + icon = self.icon + if icon is not None: + attr[ATTR_ICON] = icon + + entity_picture = self.entity_picture + if entity_picture is not None: + attr[ATTR_ENTITY_PICTURE] = entity_picture + + hidden = self.hidden + if hidden: + attr[ATTR_HIDDEN] = hidden + + assumed_state = self.assumed_state + if assumed_state: + attr[ATTR_ASSUMED_STATE] = assumed_state + + supported_features = self.supported_features + if supported_features is not None: + attr[ATTR_SUPPORTED_FEATURES] = supported_features + + device_class = self.device_class + if device_class is not None: + attr[ATTR_DEVICE_CLASS] = str(device_class) end = timer() - if not self._slow_reported and end - start > 0.4: + if end - start > 0.4 and not self._slow_reported: self._slow_reported = True _LOGGER.warning("Updating state for %s (%s) took %.3f seconds. " "Please report platform to the developers at " @@ -246,10 +266,6 @@ class Entity(object): if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) - # Remove hidden property if false so it won't show up. - if not attr.get(ATTR_HIDDEN, True): - attr.pop(ATTR_HIDDEN) - # Convert temperature if we detect one try: unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) @@ -268,7 +284,7 @@ class Entity(object): self.entity_id, state, attr, self.force_update) def schedule_update_ha_state(self, force_refresh=False): - """Schedule a update ha state change task. + """Schedule an update ha state change task. That avoid executor dead looks. """ @@ -276,7 +292,7 @@ class Entity(object): @callback def async_schedule_update_ha_state(self, force_refresh=False): - """Schedule a update ha state change task.""" + """Schedule an update ha state change task.""" self.hass.async_add_job(self.async_update_ha_state(force_refresh)) @asyncio.coroutine @@ -321,25 +337,24 @@ class Entity(object): else: self.hass.states.async_remove(self.entity_id) - def _attr_setter(self, name, typ, attr, attrs): - """Populate attributes based on properties.""" - if attr in attrs: - return - - value = getattr(self, name) - - if value is None: - return - - try: - attrs[attr] = typ(value) - except (TypeError, ValueError): - pass - def __eq__(self, other): """Return the comparison.""" - return (isinstance(other, Entity) and - other.unique_id == self.unique_id) + if not isinstance(other, self.__class__): + return False + + # Can only decide equality if both have a unique id + if self.unique_id is None or other.unique_id is None: + return False + + # Ensure they belong to the same platform + if self.platform is not None or other.platform is not None: + if self.platform is None or other.platform is None: + return False + + if self.platform.platform != other.platform.platform: + return False + + return self.unique_id == other.unique_id def __repr__(self): """Return the representation.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4a791d12e52..9dfbe580c16 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -6,24 +6,15 @@ from itertools import chain from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, - DEVICE_DEFAULT_NAME) -from homeassistant.core import callback, valid_entity_id -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady + ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import ( - async_track_time_interval, async_track_point_in_time) from homeassistant.helpers.service import extract_entity_ids from homeassistant.util import slugify -from homeassistant.util.async import ( - run_callback_threadsafe, run_coroutine_threadsafe) -import homeassistant.util.dt as dt_util +from .entity_platform import EntityPlatform DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) -SLOW_SETUP_WARNING = 10 -SLOW_SETUP_MAX_WAIT = 60 -PLATFORM_NOT_READY_RETRIES = 10 class EntityComponent(object): @@ -42,16 +33,23 @@ class EntityComponent(object): """Initialize an entity component.""" self.logger = logger self.hass = hass - self.domain = domain - self.entity_id_format = domain + '.{}' self.scan_interval = scan_interval self.group_name = group_name self.config = None self._platforms = { - 'core': EntityPlatform(self, domain, self.scan_interval, 0, None), + 'core': EntityPlatform( + hass=hass, + logger=logger, + domain=domain, + platform_name='core', + scan_interval=self.scan_interval, + parallel_updates=0, + entity_namespace=None, + async_entities_added_callback=self._async_update_group, + ) } self.async_add_entities = self._platforms['core'].async_add_entities self.add_entities = self._platforms['core'].add_entities @@ -106,17 +104,6 @@ class EntityComponent(object): discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered) - def extract_from_service(self, service, expand_group=True): - """Extract all known entities from a service call. - - Will return all entities if no entities specified in call. - Will return an empty list if entities specified but unknown. - """ - return run_callback_threadsafe( - self.hass.loop, self.async_extract_from_service, service, - expand_group - ).result() - @callback def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. @@ -135,11 +122,8 @@ class EntityComponent(object): @asyncio.coroutine def _async_setup_platform(self, platform_type, platform_config, - discovery_info=None, tries=0): - """Set up a platform for this component. - - This method must be run in the event loop. - """ + discovery_info=None): + """Set up a platform for this component.""" platform = yield from async_prepare_setup_platform( self.hass, self.config, self.domain, platform_type) @@ -160,59 +144,23 @@ class EntityComponent(object): if key not in self._platforms: entity_platform = self._platforms[key] = EntityPlatform( - self, platform_type, scan_interval, parallel_updates, - entity_namespace) + hass=self.hass, + logger=self.logger, + domain=self.domain, + platform_name=platform_type, + scan_interval=scan_interval, + parallel_updates=parallel_updates, + entity_namespace=entity_namespace, + async_entities_added_callback=self._async_update_group, + ) else: entity_platform = self._platforms[key] - self.logger.info("Setting up %s.%s", self.domain, platform_type) - warn_task = self.hass.loop.call_later( - SLOW_SETUP_WARNING, self.logger.warning, - "Setup of platform %s is taking over %s seconds.", platform_type, - SLOW_SETUP_WARNING) - - try: - if getattr(platform, 'async_setup_platform', None): - task = platform.async_setup_platform( - self.hass, platform_config, - entity_platform.async_schedule_add_entities, discovery_info - ) - else: - # This should not be replaced with hass.async_add_job because - # we don't want to track this task in case it blocks startup. - task = self.hass.loop.run_in_executor( - None, platform.setup_platform, self.hass, platform_config, - entity_platform.schedule_add_entities, discovery_info - ) - yield from asyncio.wait_for( - asyncio.shield(task, loop=self.hass.loop), - SLOW_SETUP_MAX_WAIT, loop=self.hass.loop) - yield from entity_platform.async_block_entities_done() - self.hass.config.components.add( - '{}.{}'.format(self.domain, platform_type)) - except PlatformNotReady: - tries += 1 - wait_time = min(tries, 6) * 30 - self.logger.warning( - 'Platform %s not ready yet. Retrying in %d seconds.', - platform_type, wait_time) - async_track_point_in_time( - self.hass, self._async_setup_platform( - platform_type, platform_config, discovery_info, tries), - dt_util.utcnow() + timedelta(seconds=wait_time)) - except asyncio.TimeoutError: - self.logger.error( - "Setup of platform %s is taking longer than %s seconds." - " Startup will proceed without waiting any longer.", - platform_type, SLOW_SETUP_MAX_WAIT) - except Exception: # pylint: disable=broad-except - self.logger.exception( - "Error while setting up platform %s", platform_type) - finally: - warn_task.cancel() + yield from entity_platform.async_setup( + platform, platform_config, discovery_info) @callback - def async_update_group(self): + def _async_update_group(self): """Set up and/or update component group. This method must be run in the event loop. @@ -229,12 +177,8 @@ class EntityComponent(object): visible=False, entity_ids=ids ) - def reset(self): - """Remove entities and reset the entity component to initial values.""" - run_coroutine_threadsafe(self.async_reset(), self.hass.loop).result() - @asyncio.coroutine - def async_reset(self): + def _async_reset(self): """Remove entities and reset the entity component to initial values. This method must be run in the event loop. @@ -260,11 +204,6 @@ class EntityComponent(object): if entity_id in platform.entities: yield from platform.async_remove_entity(entity_id) - def prepare_reload(self): - """Prepare reloading this entity component.""" - return run_coroutine_threadsafe( - self.async_prepare_reload(), loop=self.hass.loop).result() - @asyncio.coroutine def async_prepare_reload(self): """Prepare reloading this entity component. @@ -284,212 +223,5 @@ class EntityComponent(object): if conf is None: return None - yield from self.async_reset() + yield from self._async_reset() return conf - - -class EntityPlatform(object): - """Manage the entities for a single platform.""" - - def __init__(self, component, platform, scan_interval, parallel_updates, - entity_namespace): - """Initialize the entity platform.""" - self.component = component - self.platform = platform - self.scan_interval = scan_interval - self.parallel_updates = None - self.entity_namespace = entity_namespace - self.entities = {} - self._tasks = [] - self._async_unsub_polling = None - self._process_updates = asyncio.Lock(loop=component.hass.loop) - - if parallel_updates: - self.parallel_updates = asyncio.Semaphore( - parallel_updates, loop=component.hass.loop) - - @asyncio.coroutine - def async_block_entities_done(self): - """Wait until all entities add to hass.""" - if self._tasks: - pending = [task for task in self._tasks if not task.done()] - self._tasks.clear() - - if pending: - yield from asyncio.wait(pending, loop=self.component.hass.loop) - - def schedule_add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform.""" - run_callback_threadsafe( - self.component.hass.loop, - self.async_schedule_add_entities, list(new_entities), - update_before_add - ).result() - - @callback - def async_schedule_add_entities(self, new_entities, - update_before_add=False): - """Add entities for a single platform async.""" - self._tasks.append(self.component.hass.async_add_job( - self.async_add_entities( - new_entities, update_before_add=update_before_add) - )) - - def add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform.""" - # That avoid deadlocks - if update_before_add: - self.component.logger.warning( - "Call 'add_entities' with update_before_add=True " - "only inside tests or you can run into a deadlock!") - - run_coroutine_threadsafe( - self.async_add_entities(list(new_entities), update_before_add), - self.component.hass.loop).result() - - @asyncio.coroutine - def async_add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform async. - - This method must be run in the event loop. - """ - # handle empty list from component/platform - if not new_entities: - return - - component_entities = set(entity.entity_id for entity - in self.component.entities) - - tasks = [ - self._async_add_entity(entity, update_before_add, - component_entities) - for entity in new_entities] - - yield from asyncio.wait(tasks, loop=self.component.hass.loop) - self.component.async_update_group() - - if self._async_unsub_polling is not None or \ - not any(entity.should_poll for entity - in self.entities.values()): - return - - self._async_unsub_polling = async_track_time_interval( - self.component.hass, self._update_entity_states, self.scan_interval - ) - - @asyncio.coroutine - def _async_add_entity(self, entity, update_before_add, component_entities): - """Helper method to add an entity to the platform.""" - if entity is None: - raise ValueError('Entity cannot be None') - - # Do nothing if entity has already been added based on unique id. - if entity in self.component.entities: - return - - entity.hass = self.component.hass - entity.platform = self - entity.parallel_updates = self.parallel_updates - - # Update properties before we generate the entity_id - if update_before_add: - try: - yield from entity.async_device_update(warning=False) - except Exception: # pylint: disable=broad-except - self.component.logger.exception( - "%s: Error on device update!", self.platform) - return - - # Write entity_id to entity - if getattr(entity, 'entity_id', None) is None: - object_id = entity.name or DEVICE_DEFAULT_NAME - - if self.entity_namespace is not None: - object_id = '{} {}'.format(self.entity_namespace, - object_id) - - entity.entity_id = async_generate_entity_id( - self.component.entity_id_format, object_id, - component_entities) - - # Make sure it is valid in case an entity set the value themselves - if not valid_entity_id(entity.entity_id): - raise HomeAssistantError( - 'Invalid entity id: {}'.format(entity.entity_id)) - elif entity.entity_id in component_entities: - raise HomeAssistantError( - 'Entity id already exists: {}'.format(entity.entity_id)) - - self.entities[entity.entity_id] = entity - component_entities.add(entity.entity_id) - - if hasattr(entity, 'async_added_to_hass'): - yield from entity.async_added_to_hass() - - yield from entity.async_update_ha_state() - - @asyncio.coroutine - def async_reset(self): - """Remove all entities and reset data. - - This method must be run in the event loop. - """ - if not self.entities: - return - - tasks = [self._async_remove_entity(entity_id) - for entity_id in self.entities] - - yield from asyncio.wait(tasks, loop=self.component.hass.loop) - - if self._async_unsub_polling is not None: - self._async_unsub_polling() - self._async_unsub_polling = None - - @asyncio.coroutine - def async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - yield from self._async_remove_entity(entity_id) - - # Clean up polling job if no longer needed - if (self._async_unsub_polling is not None and - not any(entity.should_poll for entity - in self.entities.values())): - self._async_unsub_polling() - self._async_unsub_polling = None - - @asyncio.coroutine - def _async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - entity = self.entities.pop(entity_id) - - if hasattr(entity, 'async_will_remove_from_hass'): - yield from entity.async_will_remove_from_hass() - - self.component.hass.states.async_remove(entity_id) - - @asyncio.coroutine - def _update_entity_states(self, now): - """Update the states of all the polling entities. - - To protect from flooding the executor, we will update async entities - in parallel and other entities sequential. - - This method must be run in the event loop. - """ - if self._process_updates.locked(): - self.component.logger.warning( - "Updating %s %s took longer than the scheduled update " - "interval %s", self.platform, self.component.domain, - self.scan_interval) - return - - with (yield from self._process_updates): - tasks = [] - for entity in self.entities.values(): - if not entity.should_poll: - continue - tasks.append(entity.async_update_ha_state(True)) - - if tasks: - yield from asyncio.wait(tasks, loop=self.component.hass.loop) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py new file mode 100644 index 00000000000..3362f1e3b3f --- /dev/null +++ b/homeassistant/helpers/entity_platform.py @@ -0,0 +1,317 @@ +"""Class to manage the entities for a single platform.""" +import asyncio +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 ( + run_callback_threadsafe, run_coroutine_threadsafe) +import homeassistant.util.dt as dt_util + +from .event import async_track_time_interval, async_track_point_in_time +from .entity_registry import EntityRegistry + +SLOW_SETUP_WARNING = 10 +SLOW_SETUP_MAX_WAIT = 60 +PLATFORM_NOT_READY_RETRIES = 10 +DATA_REGISTRY = 'entity_registry' + + +class EntityPlatform(object): + """Manage the entities for a single platform.""" + + def __init__(self, *, hass, logger, domain, platform_name, scan_interval, + parallel_updates, entity_namespace, + async_entities_added_callback): + """Initialize the entity platform. + + hass: HomeAssistant + logger: Logger + domain: str + platform_name: str + scan_interval: timedelta + parallel_updates: int + entity_namespace: str + async_entities_added_callback: @callback method + """ + self.hass = hass + self.logger = logger + self.domain = domain + self.platform_name = platform_name + self.scan_interval = scan_interval + self.parallel_updates = None + self.entity_namespace = entity_namespace + self.async_entities_added_callback = async_entities_added_callback + self.entities = {} + self._tasks = [] + self._async_unsub_polling = None + self._process_updates = asyncio.Lock(loop=hass.loop) + + if parallel_updates: + self.parallel_updates = asyncio.Semaphore( + parallel_updates, loop=hass.loop) + + @asyncio.coroutine + def async_setup(self, platform, platform_config, discovery_info=None, + tries=0): + """Setup the platform.""" + logger = self.logger + hass = self.hass + full_name = '{}.{}'.format(self.domain, self.platform_name) + + logger.info("Setting up %s", full_name) + warn_task = hass.loop.call_later( + SLOW_SETUP_WARNING, logger.warning, + "Setup of platform %s is taking over %s seconds.", + self.platform_name, SLOW_SETUP_WARNING) + + try: + if getattr(platform, 'async_setup_platform', None): + task = platform.async_setup_platform( + hass, platform_config, + self._async_schedule_add_entities, discovery_info + ) + else: + # This should not be replaced with hass.async_add_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, platform.setup_platform, hass, platform_config, + self._schedule_add_entities, discovery_info + ) + yield from asyncio.wait_for( + asyncio.shield(task, loop=hass.loop), + SLOW_SETUP_MAX_WAIT, loop=hass.loop) + + # Block till all entities are done + if self._tasks: + pending = [task for task in self._tasks if not task.done()] + self._tasks.clear() + + if pending: + yield from asyncio.wait( + pending, loop=self.hass.loop) + + hass.config.components.add(full_name) + except PlatformNotReady: + tries += 1 + wait_time = min(tries, 6) * 30 + logger.warning( + 'Platform %s not ready yet. Retrying in %d seconds.', + self.platform_name, wait_time) + async_track_point_in_time( + hass, self.async_setup( + platform, platform_config, discovery_info, tries), + dt_util.utcnow() + timedelta(seconds=wait_time)) + except asyncio.TimeoutError: + logger.error( + "Setup of platform %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer.", + self.platform_name, SLOW_SETUP_MAX_WAIT) + except Exception: # pylint: disable=broad-except + logger.exception( + "Error while setting up platform %s", self.platform_name) + finally: + warn_task.cancel() + + def _schedule_add_entities(self, new_entities, update_before_add=False): + """Synchronously schedule adding entities for a single platform.""" + run_callback_threadsafe( + self.hass.loop, + self._async_schedule_add_entities, list(new_entities), + update_before_add + ).result() + + @callback + def _async_schedule_add_entities(self, new_entities, + update_before_add=False): + """Schedule adding entities for a single platform async.""" + self._tasks.append(self.hass.async_add_job( + self.async_add_entities( + new_entities, update_before_add=update_before_add) + )) + + def add_entities(self, new_entities, update_before_add=False): + """Add entities for a single platform.""" + # That avoid deadlocks + if update_before_add: + self.logger.warning( + "Call 'add_entities' with update_before_add=True " + "only inside tests or you can run into a deadlock!") + + run_coroutine_threadsafe( + self.async_add_entities(list(new_entities), update_before_add), + self.hass.loop).result() + + @asyncio.coroutine + def async_add_entities(self, new_entities, update_before_add=False): + """Add entities for a single platform async. + + This method must be run in the event loop. + """ + # handle empty list from component/platform + if not new_entities: + return + + hass = self.hass + component_entities = set(hass.states.async_entity_ids(self.domain)) + + registry = hass.data.get(DATA_REGISTRY) + + if registry is None: + registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass) + + yield from registry.async_ensure_loaded() + + tasks = [ + self._async_add_entity(entity, update_before_add, + component_entities, registry) + for entity in new_entities] + + yield from asyncio.wait(tasks, loop=self.hass.loop) + self.async_entities_added_callback() + + if self._async_unsub_polling is not None or \ + not any(entity.should_poll for entity + in self.entities.values()): + return + + self._async_unsub_polling = async_track_time_interval( + self.hass, self._update_entity_states, self.scan_interval + ) + + @asyncio.coroutine + def _async_add_entity(self, entity, update_before_add, component_entities, + registry): + """Helper method to add an entity to the platform.""" + if entity is None: + raise ValueError('Entity cannot be None') + + entity.hass = self.hass + entity.platform = self + entity.parallel_updates = self.parallel_updates + + # Update properties before we generate the entity_id + if update_before_add: + try: + yield from entity.async_device_update(warning=False) + except Exception: # pylint: disable=broad-except + self.logger.exception( + "%s: Error on device update!", self.platform_name) + return + + suggested_object_id = None + + # Get entity_id from unique ID registration + if entity.unique_id is not None: + if entity.entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + suggested_object_id = entity.name + + entry = registry.async_get_or_create( + self.domain, self.platform_name, entity.unique_id, + suggested_object_id=suggested_object_id) + entity.entity_id = entry.entity_id + + # We won't generate an entity ID if the platform has already set one + # We will however make sure that platform cannot pick a registered ID + elif (entity.entity_id is not None and + registry.async_is_registered(entity.entity_id)): + # If entity already registered, convert entity id to suggestion + suggested_object_id = split_entity_id(entity.entity_id)[1] + entity.entity_id = None + + # Generate entity ID + if entity.entity_id is None: + suggested_object_id = \ + suggested_object_id or entity.name or DEVICE_DEFAULT_NAME + + if self.entity_namespace is not None: + suggested_object_id = '{} {}'.format(self.entity_namespace, + suggested_object_id) + + entity.entity_id = registry.async_generate_entity_id( + self.domain, suggested_object_id) + + # Make sure it is valid in case an entity set the value themselves + if not valid_entity_id(entity.entity_id): + raise HomeAssistantError( + 'Invalid entity id: {}'.format(entity.entity_id)) + elif entity.entity_id in component_entities: + raise HomeAssistantError( + 'Entity id already exists: {}'.format(entity.entity_id)) + + self.entities[entity.entity_id] = entity + component_entities.add(entity.entity_id) + + if hasattr(entity, 'async_added_to_hass'): + yield from entity.async_added_to_hass() + + yield from entity.async_update_ha_state() + + @asyncio.coroutine + def async_reset(self): + """Remove all entities and reset data. + + This method must be run in the event loop. + """ + if not self.entities: + return + + tasks = [self._async_remove_entity(entity_id) + for entity_id in self.entities] + + yield from asyncio.wait(tasks, loop=self.hass.loop) + + if self._async_unsub_polling is not None: + self._async_unsub_polling() + self._async_unsub_polling = None + + @asyncio.coroutine + def async_remove_entity(self, entity_id): + """Remove entity id from platform.""" + yield from self._async_remove_entity(entity_id) + + # Clean up polling job if no longer needed + if (self._async_unsub_polling is not None and + not any(entity.should_poll for entity + in self.entities.values())): + self._async_unsub_polling() + self._async_unsub_polling = None + + @asyncio.coroutine + def _async_remove_entity(self, entity_id): + """Remove entity id from platform.""" + entity = self.entities.pop(entity_id) + + if hasattr(entity, 'async_will_remove_from_hass'): + yield from entity.async_will_remove_from_hass() + + self.hass.states.async_remove(entity_id) + + @asyncio.coroutine + def _update_entity_states(self, now): + """Update the states of all the polling entities. + + To protect from flooding the executor, we will update async entities + in parallel and other entities sequential. + + This method must be run in the event loop. + """ + if self._process_updates.locked(): + self.logger.warning( + "Updating %s %s took longer than the scheduled update " + "interval %s", self.platform_name, self.domain, + self.scan_interval) + return + + with (yield from self._process_updates): + tasks = [] + for entity in self.entities.values(): + if not entity.should_poll: + continue + tasks.append(entity.async_update_ha_state(True)) + + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py new file mode 100644 index 00000000000..350c8273232 --- /dev/null +++ b/homeassistant/helpers/entity_registry.py @@ -0,0 +1,134 @@ +"""Provide a registry to track entity IDs. + +The Entity Registry keeps a registry of entities. Entities are uniquely +identified by their domain, platform and a unique id provided by that platform. + +The Entity Registry will persist itself 10 seconds after a new entity is +registered. Registering a new entity while a timer is in progress resets the +timer. + +After initializing, call EntityRegistry.async_ensure_loaded to load the data +from disk. +""" +import asyncio +from collections import namedtuple, OrderedDict +from itertools import chain +import logging +import os + +from ..core import callback, split_entity_id +from ..util import ensure_unique_string, slugify +from ..util.yaml import load_yaml, save_yaml + +PATH_REGISTRY = 'entity_registry.yaml' +SAVE_DELAY = 10 +Entry = namedtuple('EntityRegistryEntry', + 'entity_id,unique_id,platform,domain') +_LOGGER = logging.getLogger(__name__) + + +class EntityRegistry: + """Class to hold a registry of entities.""" + + def __init__(self, hass): + """Initialize the registry.""" + self.hass = hass + self.entities = None + self._load_task = None + self._sched_save = None + + @callback + def async_is_registered(self, entity_id): + """Check if an entity_id is currently registered.""" + return entity_id in self.entities + + @callback + def async_generate_entity_id(self, domain, suggested_object_id): + """Generate an entity ID that does not conflict. + + Conflicts checked against registered and currently existing entities. + """ + return ensure_unique_string( + '{}.{}'.format(domain, slugify(suggested_object_id)), + chain(self.entities.keys(), + self.hass.states.async_entity_ids(domain)) + ) + + @callback + def async_get_or_create(self, domain, platform, unique_id, *, + suggested_object_id=None): + """Get entity. Create if it doesn't exist.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity + + entity_id = self.async_generate_entity_id( + domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) + entity = Entry( + entity_id=entity_id, + unique_id=unique_id, + platform=platform, + domain=domain, + ) + self.entities[entity_id] = entity + _LOGGER.info('Registered new %s.%s entity: %s', + domain, platform, entity_id) + self.async_schedule_save() + return entity + + @asyncio.coroutine + def async_ensure_loaded(self): + """Load the registry from disk.""" + if self.entities is not None: + return + + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load) + + yield from self._load_task + + @asyncio.coroutine + def _async_load(self): + """Load the entity registry.""" + path = self.hass.config.path(PATH_REGISTRY) + entities = OrderedDict() + + if os.path.isfile(path): + data = yield from self.hass.async_add_job(load_yaml, path) + + for entity_id, info in data.items(): + entities[entity_id] = Entry( + domain=split_entity_id(entity_id)[0], + entity_id=entity_id, + unique_id=info['unique_id'], + platform=info['platform'] + ) + + self.entities = entities + self._load_task = None + + @callback + def async_schedule_save(self): + """Schedule saving the entity registry.""" + if self._sched_save is not None: + self._sched_save.cancel() + + self._sched_save = self.hass.loop.call_later( + SAVE_DELAY, self.hass.async_add_job, self._async_save + ) + + @asyncio.coroutine + def _async_save(self): + """Save the entity registry to a file.""" + self._sched_save = None + data = OrderedDict() + + for entry in self.entities.values(): + data[entry.entity_id] = { + 'unique_id': entry.unique_id, + 'platform': entry.platform, + } + + yield from self.hass.async_add_job( + save_yaml, self.hass.config.path(PATH_REGISTRY), data) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 6cd1916d4c2..eab2d583f45 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,5 @@ """Helpers for listening to events.""" +from datetime import timedelta import functools as ft from homeassistant.loader import bind_hass @@ -119,7 +120,7 @@ track_template = threaded_listener_factory(async_track_template) @bind_hass def async_track_same_state(hass, period, action, async_check_same_func, entity_ids=MATCH_ALL): - """Track the state of entities for a period and run a action. + """Track the state of entities for a period and run an action. If async_check_func is None it use the state of orig_value. Without entity_ids we track all state changes. @@ -219,6 +220,14 @@ track_point_in_utc_time = threaded_listener_factory( async_track_point_in_utc_time) +@callback +@bind_hass +def async_call_later(hass, delay, action): + """Add a listener that is called in .""" + return async_track_point_in_utc_time( + hass, action, dt_util.utcnow() + timedelta(seconds=delay)) + + @callback @bind_hass def async_track_time_interval(hass, action, interval): @@ -231,7 +240,7 @@ def async_track_time_interval(hass, action, interval): @callback def interval_listener(now): - """Handle elaspsed intervals.""" + """Handle elapsed intervals.""" nonlocal remove remove = async_track_point_in_utc_time( hass, interval_listener, next_interval()) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b8b6b29df81..1ef9aa15674 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -220,7 +220,7 @@ class Script(): def async_script_timeout(now): """Call after timeout is retrieve stop script.""" self._async_listener.remove(unsub) - self._log("Timout reach, abort script.") + self._log("Timeout reached, abort script.") self.async_stop() unsub = async_track_point_in_utc_time( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f5b626c8828..b89b1689c9e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -74,6 +74,7 @@ def async_call_from_config(hass, config, blocking=False, variables=None, config[CONF_SERVICE_DATA_TEMPLATE], variables)) except TemplateError as ex: _LOGGER.error('Error rendering data template: %s', ex) + return if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 254a48c3d0a..255f760ebff 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -31,7 +31,7 @@ from homeassistant.components.cover import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, STATE_ALARM_ARMED_AWAY, @@ -78,6 +78,7 @@ SERVICE_TO_STATE = { SERVICE_TURN_OFF: STATE_OFF, SERVICE_MEDIA_PLAY: STATE_PLAYING, SERVICE_MEDIA_PAUSE: STATE_PAUSED, + SERVICE_MEDIA_STOP: STATE_IDLE, SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY, SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME, SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED, diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7eb0a602139..b381e1c2b0e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -438,7 +438,7 @@ def multiply(value, amount): def logarithm(value, base=math.e): - """Filter to get logarithm of the value with a spesific base.""" + """Filter to get logarithm of the value with a specific base.""" try: return math.log(float(value), float(base)) except (ValueError, TypeError): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ac20f94d243..a3ce2a13f56 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -19,7 +19,7 @@ import sys from types import ModuleType # pylint: disable=unused-import -from typing import Dict, Optional, Sequence, Set # NOQA +from typing import Dict, List, Optional, Sequence, Set # NOQA from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet @@ -148,7 +148,7 @@ def get_component(comp_name) -> Optional[ModuleType]: # a namespace. We do not care about namespaces. # This prevents that when only # custom_components/switch/some_platform.py exists, - # the import custom_components.switch would succeeed. + # the import custom_components.switch would succeed. if module.__spec__.origin == 'namespace': continue diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 819d8de48e0..5aa051f2bb5 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -37,7 +37,7 @@ def patch_weakref_tasks(): asyncio.tasks.Task._all_tasks = IgnoreCalls() try: del asyncio.tasks.Task.__del__ - except: + except: # noqa: E722 pass diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 243c6d418df..ee3a37bbd53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,14 +2,14 @@ requests==2.18.4 pyyaml>=3.11,<4 pytz>=2017.02 pip>=8.0.3 -jinja2>=2.9.6 +jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.7 -yarl==0.18.0 +aiohttp==2.3.10 +yarl==1.1.0 async_timeout==2.0.0 chardet==3.0.4 -astral==1.4 +astral==1.5 certifi>=2017.4.17 # Breaks Python 3.6 and is not needed for our supported Pythons diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py new file mode 100644 index 00000000000..aaf83870147 --- /dev/null +++ b/homeassistant/requirements.py @@ -0,0 +1,45 @@ +"""Module to handle installing requirements.""" +import asyncio +from functools import partial +import logging +import os + +import homeassistant.util.package as pkg_util + +DATA_PIP_LOCK = 'pip_lock' +CONSTRAINT_FILE = 'package_constraints.txt' +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_process_requirements(hass, name, requirements): + """Install the requirements for a component or platform. + + This method is a coroutine. + """ + pip_lock = hass.data.get(DATA_PIP_LOCK) + if pip_lock is None: + pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) + + pip_install = partial(pkg_util.install_package, + **pip_kwargs(hass.config.config_dir)) + + with (yield from pip_lock): + for req in requirements: + ret = yield from hass.async_add_job(pip_install, req) + if not ret: + _LOGGER.error("Not initializing %s because could not install " + "requirement %s", name, req) + return False + + return True + + +def pip_kwargs(config_dir): + """Return keyword arguments for PIP install.""" + kwargs = { + 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) + } + if not pkg_util.running_under_virtualenv(): + kwargs['target'] = os.path.join(config_dir, 'deps') + return kwargs diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index be39540682c..815a5c8e55f 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,9 +9,8 @@ from typing import List from homeassistant.bootstrap import mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant.const import CONSTRAINT_FILE -from homeassistant.util.package import ( - install_package, running_under_virtualenv) +from homeassistant import requirements +from homeassistant.util.package import install_package def run(args: List) -> int: @@ -39,17 +38,14 @@ def run(args: List) -> int: script = importlib.import_module('homeassistant.scripts.' + args[0]) config_dir = extract_config_dir() - deps_dir = mount_local_lib_path(config_dir) + mount_local_lib_path(config_dir) + pip_kwargs = requirements.pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) + for req in getattr(script, 'REQUIREMENTS', []): - if running_under_virtualenv(): - returncode = install_package(req, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) - else: - returncode = install_package( - req, target=deps_dir, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) + returncode = install_package(req, **pip_kwargs) + if not returncode: print('Aborting script, could not install dependency', req) return 1 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 27987a70ce6..5cfcf628ec5 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -13,7 +13,7 @@ from homeassistant import bootstrap, loader, setup, config as config_util import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError -REQUIREMENTS = ('colorlog==3.0.1',) +REQUIREMENTS = ('colorlog==3.1.2',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows REQUIREMENTS += ('colorama<=1',) @@ -134,7 +134,7 @@ def run(script_args: List) -> int: for sfn, sdict in res['secret_cache'].items(): sss = [] - for skey, sval in sdict.items(): + for skey in sdict: if skey in flatsecret: _LOGGER.error('Duplicated secrets in files %s and %s', flatsecret[skey], sfn) diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py index c21ac4adad9..e91aeb8a0d7 100644 --- a/homeassistant/scripts/influxdb_import.py +++ b/homeassistant/scripts/influxdb_import.py @@ -1,7 +1,8 @@ -"""Script to import recorded data into influxdb.""" +"""Script to import recorded data into an Influx database.""" import argparse import json import os +import sys from typing import List @@ -11,11 +12,13 @@ import homeassistant.config as config_util def run(script_args: List) -> int: """Run the actual script.""" from sqlalchemy import create_engine + from sqlalchemy import func from sqlalchemy.orm import sessionmaker from influxdb import InfluxDBClient from homeassistant.components.recorder import models from homeassistant.helpers import state as state_helper from homeassistant.core import State + from homeassistant.core import HomeAssistantError parser = argparse.ArgumentParser( description="import data to influxDB.") @@ -99,8 +102,8 @@ def run(script_args: List) -> int: client = None if not simulate: - client = InfluxDBClient(args.host, args.port, - args.username, args.password) + client = InfluxDBClient( + args.host, args.port, args.username, args.password) client.switch_database(args.dbname) config_dir = os.path.join(os.getcwd(), args.config) # type: str @@ -116,105 +119,162 @@ def run(script_args: List) -> int: if not os.path.exists(src_db) and not args.uri: print("Fatal Error: Database '{}' does not exist " - "and no uri given".format(src_db)) + "and no URI given".format(src_db)) return 1 - uri = args.uri or "sqlite:///{}".format(src_db) + uri = args.uri or 'sqlite:///{}'.format(src_db) engine = create_engine(uri, echo=False) session_factory = sessionmaker(bind=engine) session = session_factory() step = int(args.step) + step_start = 0 tags = {} if args.tags: - tags.update(dict(elem.split(":") for elem in args.tags.split(","))) - excl_entities = args.exclude_entities.split(",") - excl_domains = args.exclude_domains.split(",") + tags.update(dict(elem.split(':') for elem in args.tags.split(','))) + excl_entities = args.exclude_entities.split(',') + excl_domains = args.exclude_domains.split(',') override_measurement = args.override_measurement default_measurement = args.default_measurement - query = session.query(models.Events).filter( - models.Events.event_type == "state_changed").order_by( - models.Events.time_fired) + query = session.query(func.count(models.Events.event_type)).filter( + models.Events.event_type == 'state_changed') + + total_events = query.scalar() + prefix_format = '{} of {}' points = [] + invalid_points = [] count = 0 from collections import defaultdict entities = defaultdict(int) + print_progress(0, total_events, prefix_format.format(0, total_events)) - for event in query: - event_data = json.loads(event.event_data) - state = State.from_dict(event_data.get("new_state")) + while True: - if not state or ( - excl_entities and state.entity_id in excl_entities) or ( - excl_domains and state.domain in excl_domains): - session.expunge(event) - continue + step_stop = step_start + step + if step_start > total_events: + print_progress(total_events, total_events, prefix_format.format( + total_events, total_events)) + break + query = session.query(models.Events).filter( + models.Events.event_type == 'state_changed').order_by( + models.Events.time_fired).slice(step_start, step_stop) - try: - _state = float(state_helper.state_as_number(state)) - _state_key = "value" - except ValueError: - _state = state.state - _state_key = "state" + for event in query: + event_data = json.loads(event.event_data) - if override_measurement: - measurement = override_measurement - else: - measurement = state.attributes.get('unit_of_measurement') - if measurement in (None, ''): - if default_measurement: - measurement = default_measurement - else: - measurement = state.entity_id + if not ('entity_id' in event_data) or ( + excl_entities and event_data[ + 'entity_id'] in excl_entities) or ( + excl_domains and event_data[ + 'entity_id'].split('.')[0] in excl_domains): + session.expunge(event) + continue - point = { - 'measurement': measurement, - 'tags': { - 'domain': state.domain, - 'entity_id': state.object_id, - }, - 'time': event.time_fired, - 'fields': { - _state_key: _state, + try: + state = State.from_dict(event_data.get('new_state')) + except HomeAssistantError: + invalid_points.append(event_data) + + if not state: + invalid_points.append(event_data) + continue + + try: + _state = float(state_helper.state_as_number(state)) + _state_key = 'value' + except ValueError: + _state = state.state + _state_key = 'state' + + if override_measurement: + measurement = override_measurement + else: + measurement = state.attributes.get('unit_of_measurement') + if measurement in (None, ''): + if default_measurement: + measurement = default_measurement + else: + measurement = state.entity_id + + point = { + 'measurement': measurement, + 'tags': { + 'domain': state.domain, + 'entity_id': state.object_id, + }, + 'time': event.time_fired, + 'fields': { + _state_key: _state, + } } - } - for key, value in state.attributes.items(): - if key != 'unit_of_measurement': - # If the key is already in fields - if key in point['fields']: - key = key + "_" - # Prevent column data errors in influxDB. - # For each value we try to cast it as float - # But if we can not do it we store the value - # as string add "_str" postfix to the field key - try: - point['fields'][key] = float(value) - except (ValueError, TypeError): - new_key = "{}_str".format(key) - point['fields'][new_key] = str(value) + for key, value in state.attributes.items(): + if key != 'unit_of_measurement': + # If the key is already in fields + if key in point['fields']: + key = key + '_' + # Prevent column data errors in influxDB. + # For each value we try to cast it as float + # But if we can not do it we store the value + # as string add "_str" postfix to the field key + try: + point['fields'][key] = float(value) + except (ValueError, TypeError): + new_key = '{}_str'.format(key) + point['fields'][new_key] = str(value) - entities[state.entity_id] += 1 - point['tags'].update(tags) - points.append(point) - session.expunge(event) - if len(points) >= step: + entities[state.entity_id] += 1 + point['tags'].update(tags) + points.append(point) + session.expunge(event) + + if points: if not simulate: - print("Write {} points to the database".format(len(points))) client.write_points(points) count += len(points) - points = [] + # This prevents the progress bar from going over 100% when + # the last step happens + print_progress((step_start + len( + points)), total_events, prefix_format.format( + step_start, total_events)) + else: + print_progress( + (step_start + step), total_events, prefix_format.format( + step_start, total_events)) - if points: - if not simulate: - print("Write {} points to the database".format(len(points))) - client.write_points(points) - count += len(points) + points = [] + step_start += step print("\nStatistics:") print("\n".join(["{:6}: {}".format(v, k) for k, v in sorted(entities.items(), key=lambda x: x[1])])) - print("\nImport finished {} points written".format(count)) + print("\nInvalid Points: {}".format(len(invalid_points))) + print("\nImport finished: {} points written".format(count)) return 0 + + +# Based on code at +# http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console +def print_progress(iteration: int, total: int, prefix: str='', suffix: str='', + decimals: int=2, bar_length: int=68) -> None: + """Print progress bar. + + Call in a loop to create terminal progress bar + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : number of decimals in percent complete (Int) + barLength - Optional : character length of bar (Int) + """ + filled_length = int(round(bar_length * iteration / float(total))) + percents = round(100.00 * (iteration / float(total)), decimals) + line = '#' * filled_length + '-' * (bar_length - filled_length) + sys.stdout.write('%s [%s] %s%s %s\r' % (prefix, line, + percents, '%', suffix)) + sys.stdout.flush() + if iteration == total: + print('\n') diff --git a/homeassistant/scripts/influxdb_migrator.py b/homeassistant/scripts/influxdb_migrator.py index cad8f878ca6..f41240bad74 100644 --- a/homeassistant/scripts/influxdb_migrator.py +++ b/homeassistant/scripts/influxdb_migrator.py @@ -119,7 +119,7 @@ def run(script_args: List) -> int: point_wt_time = 0 print("Migrating from {} to {}".format(old_dbname, args.dbname)) - # Walk into measurenebt + # Walk into measurement for index, measurement in enumerate(measurements): # Get tag list diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index b46d135c107..64ad09bcd70 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==10.6.0', 'keyrings.alt==2.3'] +REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3'] def run(args): @@ -29,7 +29,7 @@ def run(args): if args.action == 'info': keyr = keyring.get_keyring() - print('Keyring version {}\n'.format(keyring.__version__)) + print('Keyring version {}\n'.format(REQUIREMENTS[0].split('==')[1])) print('Active keyring : {}'.format(keyr.__module__)) config_name = os.path.join(platform.config_root(), 'keyringrc.cfg') print('Config location : {}'.format(config_name)) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12a39e80517..3221ea35d48 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -1,27 +1,24 @@ """All methods needed to bootstrap a Home Assistant instance.""" import asyncio import logging.handlers -import os from timeit import default_timer as timer from types import ModuleType from typing import Optional, Dict -import homeassistant.config as conf_util -import homeassistant.core as core -import homeassistant.loader as loader -import homeassistant.util.package as pkg_util +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, CONSTRAINT_FILE) +from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async import run_coroutine_threadsafe + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = 'component' DATA_SETUP = 'setup_tasks' -DATA_PIP_LOCK = 'pip_lock' +DATA_DEPS_REQS = 'deps_reqs_processed' SLOW_SETUP_WARNING = 10 @@ -60,43 +57,6 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, return (yield from task) -@asyncio.coroutine -def _async_process_requirements(hass: core.HomeAssistant, name: str, - requirements) -> bool: - """Install the requirements for a component. - - This method is a coroutine. - """ - if hass.config.skip_pip: - return True - - pip_lock = hass.data.get(DATA_PIP_LOCK) - if pip_lock is None: - pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) - - def pip_install(mod): - """Install packages.""" - if pkg_util.running_under_virtualenv(): - return pkg_util.install_package( - mod, constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - return pkg_util.install_package( - mod, target=hass.config.path('deps'), - constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - - with (yield from pip_lock): - for req in requirements: - ret = yield from hass.async_add_job(pip_install, req) - if not ret: - _LOGGER.error("Not initializing %s because could not install " - "dependency %s", name, req) - async_notify_setup_error(hass, name) - return False - - return True - - @asyncio.coroutine def _async_process_dependencies(hass, config, name, dependencies): """Ensure all dependencies are set up.""" @@ -162,22 +122,11 @@ def _async_setup_component(hass: core.HomeAssistant, log_error("Invalid config.") return False - if not hass.config.skip_pip and hasattr(component, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, domain, component.REQUIREMENTS) - if not req_success: - log_error("Could not install all requirements.") - return False - - if hasattr(component, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, domain, component.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return False - - async_comp = hasattr(component, 'async_setup') + try: + yield from _process_deps_reqs(hass, config, domain, component) + except HomeAssistantError as err: + log_error(str(err)) + return False start = timer() _LOGGER.info("Setting up %s", domain) @@ -192,7 +141,7 @@ def _async_setup_component(hass: core.HomeAssistant, domain, SLOW_SETUP_WARNING) try: - if async_comp: + if hasattr(component, 'async_setup'): result = yield from component.async_setup(hass, processed_config) else: result = yield from hass.async_add_job( @@ -256,21 +205,40 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, elif platform_path in hass.config.components: return platform - # Load dependencies - if hasattr(platform, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, platform_path, platform.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return None - - if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, platform_path, platform.REQUIREMENTS) - - if not req_success: - log_error("Could not install all requirements.") - return None + try: + yield from _process_deps_reqs(hass, config, platform_name, platform) + except HomeAssistantError as err: + log_error(str(err)) + return None return platform + + +@asyncio.coroutine +def _process_deps_reqs(hass, config, name, module): + """Process all dependencies and requirements for a module. + + Module is a Python module of either a component or platform. + """ + processed = hass.data.get(DATA_DEPS_REQS) + + if processed is None: + processed = hass.data[DATA_DEPS_REQS] = set() + elif name in processed: + return + + if hasattr(module, 'DEPENDENCIES'): + dep_success = yield from _async_process_dependencies( + hass, config, name, module.DEPENDENCIES) + + if not dep_success: + raise HomeAssistantError("Could not setup all dependencies.") + + if not hass.config.skip_pip and hasattr(module, 'REQUIREMENTS'): + req_success = yield from requirements.async_process_requirements( + hass, name, module.REQUIREMENTS) + + if not req_success: + raise HomeAssistantError("Could not install all requirements.") + + processed.add(name) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index cb3ebeb7ee6..c4fea2846c5 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -227,7 +227,7 @@ class OrderedSet(MutableSet): return '%s(%r)' % (self.__class__.__name__, list(self)) def __eq__(self, other): - """Return the comparision.""" + """Return the comparison.""" if isinstance(other, OrderedSet): return len(self) == len(other) and list(self) == list(other) return set(self) == set(other) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 5e8b3382fb1..c3400bac9be 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -3,7 +3,7 @@ import datetime as dt import re # pylint: disable=unused-import -from typing import Any, Union, Optional, Tuple # NOQA +from typing import Any, Dict, Union, Optional, Tuple # NOQA import pytz diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 8b07a344148..35b266cb104 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -107,7 +107,7 @@ def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], sinU2 = math.sin(U2) cosU2 = math.cos(U2) - for iteration in range(MAX_ITERATIONS): + for _ in range(MAX_ITERATIONS): sinLambda = math.sin(Lambda) cosLambda = math.cos(Lambda) sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 7daaf937975..8a15c4f6320 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -23,7 +23,7 @@ class HideSensitiveDataFilter(logging.Filter): # pylint: disable=invalid-name class AsyncHandler(object): - """Logging handler wrapper to add a async layer.""" + """Logging handler wrapper to add an async layer.""" def __init__(self, loop, handler): """Initialize async logging handler wrapper.""" diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 31b76365da4..ecef1087747 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -105,7 +105,7 @@ class UnitSystem(object): raise TypeError('{} is not a numeric value.'.format(str(length))) return distance_util.convert(length, from_unit, - self.length_unit) # type: float + self.length_unit) def as_dict(self) -> dict: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 48d709bc549..d0d5199e0f4 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -83,6 +83,14 @@ def dump(_dict: dict) -> str: .replace(': null\n', ':\n') +def save_yaml(path, data): + """Save YAML to a file.""" + # Dump before writing to not truncate the file if dumping fails + data = dump(data) + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(data) + + def clear_secret_cache() -> None: """Clear the secret cache. diff --git a/requirements_all.txt b/requirements_all.txt index ad6602464a6..3ca5b9fc763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,14 +3,14 @@ requests==2.18.4 pyyaml>=3.11,<4 pytz>=2017.02 pip>=8.0.3 -jinja2>=2.9.6 +jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.7 -yarl==0.18.0 +aiohttp==2.3.10 +yarl==1.1.0 async_timeout==2.0.0 chardet==3.0.4 -astral==1.4 +astral==1.5 certifi>=2017.4.17 # homeassistant.components.nuimo_controller @@ -50,7 +50,7 @@ SoCo==0.13 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.6 +TwitterAPI==2.4.8 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 @@ -126,7 +126,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.4.0 +bellows==0.5.0 # homeassistant.components.blink blinkpy==0.6.0 @@ -170,13 +170,13 @@ caldav==0.5.0 ciscosparkapi==0.4.2 # homeassistant.components.coinbase -coinbase==2.0.6 +coinbase==2.0.7 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.1.2 +coinmarketcap==4.2.1 # homeassistant.scripts.check_config -colorlog==3.0.1 +colorlog==3.1.2 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180130.0 +home-assistant-frontend==20180209.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -406,7 +406,7 @@ ihcsdk==2.1.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb -influxdb==4.1.1 +influxdb==5.0.0 # homeassistant.components.insteon_local insteonlocal==0.53 @@ -425,7 +425,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.5 # homeassistant.scripts.keyring -keyring==10.6.0 +keyring==11.0.0 # homeassistant.scripts.keyring keyrings.alt==2.3 @@ -441,7 +441,7 @@ libpurecoollink==0.4.2 libpyfoscam==1.0 # homeassistant.components.device_tracker.mikrotik -librouteros==1.0.4 +librouteros==1.0.5 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 @@ -453,7 +453,7 @@ liffylights==0.9.4 lightify==1.0.6.1 # homeassistant.components.light.limitlessled -limitlessled==1.0.8 +limitlessled==1.0.9 # homeassistant.components.linode linode-api==4.1.4b2 @@ -477,6 +477,9 @@ matrix-client==0.0.6 # homeassistant.components.maxcube maxcube-api==0.1.0 +# homeassistant.components.mercedesme +mercedesmejsonpy==0.1.2 + # homeassistant.components.notify.message_bird messagebird==1.2.0 @@ -485,7 +488,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.2.0 +miflora==0.3.0 # homeassistant.components.upnp miniupnpc==2.0.2 @@ -494,7 +497,7 @@ miniupnpc==2.0.2 motorparts==1.0.2 # homeassistant.components.tts -mutagen==1.39 +mutagen==1.40.0 # homeassistant.components.mychevy mychevy==0.1.1 @@ -551,7 +554,7 @@ orvibo==1.1.1 paho-mqtt==1.3.1 # homeassistant.components.media_player.panasonic_viera -panasonic_viera==0.2 +panasonic_viera==0.3 # homeassistant.components.media_player.dunehd pdunehd==1.3 @@ -615,11 +618,14 @@ pushetta==1.0.15 pwmled==1.2.1 # homeassistant.components.canary -py-canary==0.2.3 +py-canary==0.4.0 # homeassistant.components.sensor.cpuspeed py-cpuinfo==3.3.0 +# homeassistant.components.melissa +py-melissa-climate==1.0.1 + # homeassistant.components.camera.synology py-synology==0.1.5 @@ -687,7 +693,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==25 +pydeconz==27 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -699,7 +705,7 @@ pydroid-ipcam==0.8 pyebox==0.1.0 # homeassistant.components.climate.econet -pyeconet==0.0.4 +pyeconet==0.0.5 # homeassistant.components.eight_sleep pyeight==0.0.7 @@ -732,7 +738,7 @@ pyhik==0.1.4 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.38 +pyhomematic==0.1.39 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 @@ -783,8 +789,11 @@ pylutron==0.1.0 # homeassistant.components.notify.mailgun pymailgunner==1.4 +# homeassistant.components.media_player.mediaroom +pymediaroom==0.5 + # homeassistant.components.mochad -pymochad==0.1.1 +pymochad==0.2.0 # homeassistant.components.modbus pymodbus==1.3.1 @@ -821,7 +830,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.iota -pyota==2.0.3 +pyota==2.0.4 # homeassistant.components.sensor.otp pyotp==2.2.6 @@ -830,6 +839,9 @@ pyotp==2.2.6 # homeassistant.components.weather.openweathermap pyowm==2.8.0 +# homeassistant.components.sensor.pollen +pypollencom==1.1.1 + # homeassistant.components.qwikswitch pyqwikswitch==0.4 @@ -848,6 +860,9 @@ pyserial==3.1.1 # homeassistant.components.lock.sesame pysesame==0.1.0 +# homeassistant.components.goalfeed +pysher==0.2.0 + # homeassistant.components.sensor.sma pysma==0.1.3 @@ -903,9 +918,10 @@ python-juicenet==0.0.5 # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.4 +python-miio==0.3.5 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 @@ -957,7 +973,7 @@ python-wink==1.7.3 python_opendata_transport==0.0.3 # homeassistant.components.zwave -python_openzwave==0.4.0.35 +python_openzwave==0.4.3 # homeassistant.components.alarm_control_panel.egardia pythonegardia==1.0.26 @@ -969,7 +985,7 @@ pythonwhois==2.4.3 pytile==1.1.0 # homeassistant.components.climate.touchline -pytouchline==0.6 +pytouchline==0.7 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 @@ -999,7 +1015,7 @@ pywebpush==1.5.0 pywemo==0.4.25 # homeassistant.components.camera.xeoma -pyxeoma==1.2 +pyxeoma==1.3 # homeassistant.components.zabbix pyzabbix==0.7.4 @@ -1056,7 +1072,7 @@ samsungctl==0.6.0 satel_integra==0.1.0 # homeassistant.components.sensor.deutsche_bahn -schiene==0.20 +schiene==0.21 # homeassistant.components.scsgate scsgate==0.1.0 @@ -1111,7 +1127,8 @@ speedtest-cli==1.0.7 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.2.1 +# homeassistant.components.sensor.sql +sqlalchemy==1.2.2 # homeassistant.components.statsd statsd==3.2.1 @@ -1123,7 +1140,7 @@ steamodd==4.21 suds-py3==1.3.3.0 # homeassistant.components.tahoma -tahoma-api==0.0.10 +tahoma-api==0.0.11 # homeassistant.components.sensor.tank_utility tank_utility==1.4.0 @@ -1144,7 +1161,7 @@ tellduslive==0.10.4 temperusb==1.5.3 # homeassistant.components.tesla -teslajsonpy==0.0.19 +teslajsonpy==0.0.23 # homeassistant.components.thingspeak thingspeak==0.4.1 @@ -1181,7 +1198,7 @@ user-agents==1.1.0 uvcclient==0.10.1 # homeassistant.components.climate.venstar -venstarcolortouch==0.5 +venstarcolortouch==0.6 # homeassistant.components.volvooncall volvooncall==0.4.0 @@ -1198,9 +1215,8 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan # homeassistant.components.media_player.panasonic_viera # homeassistant.components.media_player.samsungtv -# homeassistant.components.media_player.webostv # homeassistant.components.switch.wake_on_lan -wakeonlan==0.2.2 +wakeonlan==1.0.0 # homeassistant.components.sensor.waqi waqiasync==1.0.0 @@ -1209,7 +1225,7 @@ waqiasync==1.0.0 warrant==0.6.1 # homeassistant.components.waterfurnace -waterfurnace==0.3.0 +waterfurnace==0.4.0 # homeassistant.components.media_player.gpmdp websocket-client==0.37.0 @@ -1247,7 +1263,7 @@ yeelight==0.3.3 yeelightsunflower==0.0.8 # homeassistant.components.media_extractor -youtube_dl==2018.01.14 +youtube_dl==2018.01.21 # homeassistant.components.light.zengge zengge==0.2 @@ -1257,3 +1273,9 @@ zeroconf==0.19.1 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 + +# homeassistant.components.zha +zigpy-xbee==0.0.1 + +# homeassistant.components.zha +zigpy==0.0.1 diff --git a/requirements_docs.txt b/requirements_docs.txt index 04ebb074e03..c5c48e0bc73 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.6.6 -sphinx-autodoc-typehints==1.2.3 +Sphinx==1.6.7 +sphinx-autodoc-typehints==1.2.4 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test.txt b/requirements_test.txt index 22bb6623e16..cddf11a34b8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ # 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.3 +flake8==3.5 pylint==1.6.5 mypy==0.560 pydocstyle==1.1.1 @@ -13,5 +13,5 @@ pytest-timeout>=1.2.1 pytest-sugar==0.9.0 requests_mock==1.4 mock-open==1.3.1 -flake8-docstrings==1.0.2 +flake8-docstrings==1.0.3 asynctest>=0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c162dc2fd02..1ae1b9f2e14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,7 +2,7 @@ # 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.3 +flake8==3.5 pylint==1.6.5 mypy==0.560 pydocstyle==1.1.1 @@ -14,7 +14,7 @@ pytest-timeout>=1.2.1 pytest-sugar==0.9.0 requests_mock==1.4 mock-open==1.3.1 -flake8-docstrings==1.0.2 +flake8-docstrings==1.0.3 asynctest>=0.11.1 @@ -38,7 +38,7 @@ apns2==0.3.0 caldav==0.5.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.1.2 +coinmarketcap==4.2.1 # homeassistant.components.device_tracker.upc_connect defusedxml==0.5.0 @@ -75,11 +75,11 @@ hbmqtt==0.9.1 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180130.0 +home-assistant-frontend==20180209.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb -influxdb==4.1.1 +influxdb==5.0.0 # homeassistant.components.dyson libpurecoollink==0.4.2 @@ -121,7 +121,7 @@ prometheus_client==0.1.0 pushbullet.py==0.11.0 # homeassistant.components.canary -py-canary==0.2.3 +py-canary==0.4.0 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -169,7 +169,8 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.2.1 +# homeassistant.components.sensor.sql +sqlalchemy==1.2.2 # homeassistant.components.statsd statsd==3.2.1 @@ -183,9 +184,8 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan # homeassistant.components.media_player.panasonic_viera # homeassistant.components.media_player.samsungtv -# homeassistant.components.media_player.webostv # homeassistant.components.switch.wake_on_lan -wakeonlan==0.2.2 +wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 diff --git a/setup.py b/setup.py index 4b19e47fb2c..5af84fc8e0e 100755 --- a/setup.py +++ b/setup.py @@ -2,14 +2,16 @@ """Home Assistant setup script.""" import os from setuptools import setup, find_packages +import sys + +import homeassistant.const as hass_const -from homeassistant.const import __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2017, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) PROJECT_URL = 'https://home-assistant.io/' PROJECT_EMAIL = 'hello@home-assistant.io' PROJECT_DESCRIPTION = ('Open-source home automation platform ' @@ -41,7 +43,7 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) HERE = os.path.abspath(os.path.dirname(__file__)) -DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, __version__) +DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) @@ -50,20 +52,26 @@ REQUIRES = [ 'pyyaml>=3.11,<4', 'pytz>=2017.02', 'pip>=8.0.3', - 'jinja2>=2.9.6', + 'jinja2>=2.10', 'voluptuous==0.10.5', 'typing>=3,<4', - 'aiohttp==2.3.7', # If updated, check if yarl also needs an update! - 'yarl==0.18.0', + 'aiohttp==2.3.10', # If updated, check if yarl also needs an update! + 'yarl==1.1.0', 'async_timeout==2.0.0', 'chardet==3.0.4', - 'astral==1.4', + 'astral==1.5', 'certifi>=2017.4.17', ] +MIN_PY_VERSION = '.'.join(map( + str, + hass_const.REQUIRED_PYTHON_VER_WIN + if sys.platform.startswith('win') + else hass_const.REQUIRED_PYTHON_VER)) + setup( name=PROJECT_PACKAGE_NAME, - version=__version__, + version=hass_const.__version__, license=PROJECT_LICENSE, url=PROJECT_URL, download_url=DOWNLOAD_URL, @@ -75,6 +83,7 @@ setup( zip_safe=False, platforms='any', install_requires=REQUIRES, + python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', keywords=['home', 'automation'], entry_points={ diff --git a/tests/common.py b/tests/common.py index 3823a1e2b4e..22af8ecb8a3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,7 +14,9 @@ from aiohttp import web from homeassistant import core as ha, loader from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config -from homeassistant.helpers import intent, dispatcher, entity, restore_state +from homeassistant.helpers import ( + intent, dispatcher, entity, restore_state, entity_registry, + entity_platform) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util import homeassistant.util.yaml as yaml @@ -315,6 +317,14 @@ def mock_component(hass, component): hass.config.components.add(component) +def mock_registry(hass): + """Mock the Entity Registry.""" + registry = entity_registry.EntityRegistry(hass) + registry.entities = {} + hass.data[entity_platform.DATA_REGISTRY] = registry + return registry + + class MockModule(object): """Representation of a fake module.""" @@ -576,3 +586,40 @@ class MockDependency: func(*args, **kwargs) return run_mocked + + +class MockEntity(entity.Entity): + """Mock Entity class.""" + + def __init__(self, **values): + """Initialize an entity.""" + self._values = values + + if 'entity_id' in values: + self.entity_id = values['entity_id'] + + @property + def name(self): + """Return the name of the entity.""" + return self._handle('name') + + @property + def should_poll(self): + """Return the ste of the polling.""" + return self._handle('should_poll') + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._handle('unique_id') + + @property + def available(self): + """Return True if entity is available.""" + return self._handle('available') + + def _handle(self, attr): + """Helper for the attributes.""" + if attr in self._values: + return self._values[attr] + return getattr(super(), attr) diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index c47ed941b65..e4b29d43e48 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -34,7 +34,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): mock = MagicMock() add_devices = mock.MagicMock() demo.setup_platform(self.hass, {}, add_devices) - self.assertEquals(add_devices.call_count, 1) + self.assertEqual(add_devices.call_count, 1) def test_arm_home_no_pending(self): """Test arm home method.""" diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index a3587622b3d..2c8fafde155 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -98,8 +98,9 @@ def alexa_client(loop, hass, test_client): return loop.run_until_complete(test_client(hass.http.app)) -def _intent_req(client, data={}): - return client.post(intent.INTENTS_API_ENDPOINT, data=json.dumps(data), +def _intent_req(client, data=None): + return client.post(intent.INTENTS_API_ENDPOINT, + data=json.dumps(data or {}), headers={'content-type': 'application/json'}) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3416dfbe367..71485231150 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,7 +5,9 @@ from uuid import uuid4 import pytest -from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + TEMP_FAHRENHEIT, STATE_LOCKED, STATE_UNLOCKED, + STATE_UNKNOWN) from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import smart_home @@ -102,84 +104,12 @@ def test_wrong_version(hass): @asyncio.coroutine -def test_discovery_request(hass): +def discovery_test(device, hass, expected_endpoints=1): """Test alexa discovery request.""" request = get_new_request('Alexa.Discovery', 'Discover') # setup test devices - hass.states.async_set( - 'switch.test', 'on', {'friendly_name': "Test switch"}) - - hass.states.async_set( - 'light.test_1', 'on', {'friendly_name': "Test light 1"}) - hass.states.async_set( - 'light.test_2', 'on', { - 'friendly_name': "Test light 2", 'supported_features': 1 - }) - hass.states.async_set( - 'light.test_3', 'on', { - 'friendly_name': "Test light 3", 'supported_features': 19 - }) - - hass.states.async_set( - 'script.test', 'off', {'friendly_name': "Test script"}) - hass.states.async_set( - 'script.test_2', 'off', {'friendly_name': "Test script 2", - 'can_cancel': True}) - - hass.states.async_set( - 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"}) - - hass.states.async_set( - 'scene.test', 'off', {'friendly_name': "Test scene"}) - - hass.states.async_set( - 'fan.test_1', 'off', {'friendly_name': "Test fan 1"}) - - hass.states.async_set( - 'fan.test_2', 'off', { - 'friendly_name': "Test fan 2", 'supported_features': 1, - 'speed_list': ['low', 'medium', 'high'] - }) - - hass.states.async_set( - 'lock.test', 'off', {'friendly_name': "Test lock"}) - - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", - 'supported_features': 20925, - 'volume_level': 1 - }) - - hass.states.async_set( - 'alert.test', 'off', {'friendly_name': "Test alert"}) - - hass.states.async_set( - 'automation.test', 'off', {'friendly_name': "Test automation"}) - - hass.states.async_set( - 'group.test', 'off', {'friendly_name': "Test group"}) - - hass.states.async_set( - 'cover.test', 'off', { - 'friendly_name': "Test cover", 'supported_features': 255, - 'position': 85 - }) - - hass.states.async_set( - 'sensor.test_temp', '59', { - 'friendly_name': "Test Temp Sensor", - 'unit_of_measurement': TEMP_FAHRENHEIT, - }) - - # This sensor measures a quantity not applicable to Alexa, and should not - # be discovered. - hass.states.async_set( - 'sensor.test_sickness', '0.1', { - 'friendly_name': "Test Space Sickness Sensor", - 'unit_of_measurement': 'garn', - }) + hass.states.async_set(*device) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) @@ -187,180 +117,580 @@ def test_discovery_request(hass): assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 17 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' + endpoints = msg['payload']['endpoints'] + assert len(endpoints) == expected_endpoints - for appliance in msg['payload']['endpoints']: - if appliance['endpointId'] == 'switch#test': - assert appliance['displayCategories'][0] == "SWITCH" - assert appliance['friendlyName'] == "Test switch" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue + if expected_endpoints == 1: + return endpoints[0] + elif expected_endpoints > 1: + return endpoints + return None - if appliance['endpointId'] == 'light#test_1': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 1" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - if appliance['endpointId'] == 'light#test_2': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 2" - assert len(appliance['capabilities']) == 2 +def assert_endpoint_capabilities(endpoint, *interfaces): + """Assert the endpoint supports the given interfaces. - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) + Returns a set of capabilities, in case you want to assert more things about + them. + """ + capabilities = endpoint['capabilities'] + supported = set( + feature['interface'] + for feature in capabilities) - assert 'Alexa.BrightnessController' in caps - assert 'Alexa.PowerController' in caps + assert supported == set(interfaces) + return capabilities - continue - if appliance['endpointId'] == 'light#test_3': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 3" - assert len(appliance['capabilities']) == 4 +@asyncio.coroutine +def test_switch(hass): + """Test switch discovery.""" + device = ('switch.test', 'on', {'friendly_name': "Test switch"}) + appliance = yield from discovery_test(device, hass) - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) + assert appliance['endpointId'] == 'switch#test' + assert appliance['displayCategories'][0] == "SWITCH" + assert appliance['friendlyName'] == "Test switch" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') - assert 'Alexa.BrightnessController' in caps - assert 'Alexa.PowerController' in caps - assert 'Alexa.ColorController' in caps - assert 'Alexa.ColorTemperatureController' in caps + yield from assert_power_controller_works( + 'switch#test', + 'switch.turn_on', + 'switch.turn_off', + hass) - continue + properties = yield from reported_properties(hass, 'switch#test') + properties.assert_equal('Alexa.PowerController', 'powerState', 'ON') - if appliance['endpointId'] == 'script#test': - assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" - assert appliance['friendlyName'] == "Test script" - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['interface'] == 'Alexa.SceneController' - assert not capability['supportsDeactivation'] - continue - if appliance['endpointId'] == 'script#test_2': - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['supportsDeactivation'] - continue +@asyncio.coroutine +def test_light(hass): + """Test light discovery.""" + device = ('light.test_1', 'on', {'friendly_name': "Test light 1"}) + appliance = yield from discovery_test(device, hass) - if appliance['endpointId'] == 'input_boolean#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test input boolean" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue + assert appliance['endpointId'] == 'light#test_1' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 1" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') - if appliance['endpointId'] == 'scene#test': - assert appliance['displayCategories'][0] == "SCENE_TRIGGER" - assert appliance['friendlyName'] == "Test scene" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.SceneController' - continue + yield from assert_power_controller_works( + 'light#test_1', + 'light.turn_on', + 'light.turn_off', + hass) - if appliance['endpointId'] == 'fan#test_1': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test fan 1" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - if appliance['endpointId'] == 'fan#test_2': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test fan 2" - assert len(appliance['capabilities']) == 2 +@asyncio.coroutine +def test_dimmable_light(hass): + """Test dimmable light discovery.""" + device = ( + 'light.test_2', 'on', { + 'brightness': 128, + 'friendly_name': "Test light 2", 'supported_features': 1 + }) + appliance = yield from discovery_test(device, hass) - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) + assert appliance['endpointId'] == 'light#test_2' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 2" - assert 'Alexa.PercentageController' in caps - assert 'Alexa.PowerController' in caps - continue + assert_endpoint_capabilities( + appliance, + 'Alexa.BrightnessController', + 'Alexa.PowerController', + ) - if appliance['endpointId'] == 'lock#test': - assert appliance['displayCategories'][0] == "SMARTLOCK" - assert appliance['friendlyName'] == "Test lock" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.LockController' - continue + properties = yield from reported_properties(hass, 'light#test_2') + properties.assert_equal('Alexa.PowerController', 'powerState', 'ON') + properties.assert_equal('Alexa.BrightnessController', 'brightness', 50) - if appliance['endpointId'] == 'media_player#test': - assert appliance['displayCategories'][0] == "TV" - assert appliance['friendlyName'] == "Test media player" - assert len(appliance['capabilities']) == 3 - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) + call, _ = yield from assert_request_calls_service( + 'Alexa.BrightnessController', 'SetBrightness', 'light#test_2', + 'light.turn_on', + hass, + payload={'brightness': '50'}) + assert call.data['brightness_pct'] == 50 - assert 'Alexa.PowerController' in caps - assert 'Alexa.Speaker' in caps - assert 'Alexa.PlaybackController' in caps - continue - if appliance['endpointId'] == 'alert#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test alert" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue +@asyncio.coroutine +def test_color_light(hass): + """Test color light discovery.""" + device = ( + 'light.test_3', + 'on', + { + 'friendly_name': "Test light 3", + 'supported_features': 19, + 'min_mireds': 142, + 'color_temp': '333', + } + ) + appliance = yield from discovery_test(device, hass) - if appliance['endpointId'] == 'automation#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test automation" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue + assert appliance['endpointId'] == 'light#test_3' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 3" - if appliance['endpointId'] == 'group#test': - assert appliance['displayCategories'][0] == "SCENE_TRIGGER" - assert appliance['friendlyName'] == "Test group" - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['interface'] == 'Alexa.SceneController' - assert capability['supportsDeactivation'] is True - continue + assert_endpoint_capabilities( + appliance, + 'Alexa.BrightnessController', + 'Alexa.PowerController', + 'Alexa.ColorController', + 'Alexa.ColorTemperatureController', + ) - if appliance['endpointId'] == 'cover#test': - assert appliance['displayCategories'][0] == "DOOR" - assert appliance['friendlyName'] == "Test cover" - assert len(appliance['capabilities']) == 2 + # IncreaseColorTemperature and DecreaseColorTemperature have their own + # tests - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) - assert 'Alexa.PercentageController' in caps - assert 'Alexa.PowerController' in caps - continue +@asyncio.coroutine +def test_script(hass): + """Test script discovery.""" + device = ('script.test', 'off', {'friendly_name': "Test script"}) + appliance = yield from discovery_test(device, hass) - if appliance['endpointId'] == 'sensor#test_temp': - assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' - assert appliance['friendlyName'] == 'Test Temp Sensor' - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][0] - assert capability['interface'] == 'Alexa.TemperatureSensor' - assert capability['retrievable'] is True - properties = capability['properties'] - assert {'name': 'temperature'} in properties['supported'] - continue + assert appliance['endpointId'] == 'script#test' + assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" + assert appliance['friendlyName'] == "Test script" - raise AssertionError("Unknown appliance!") + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert not capability['supportsDeactivation'] + + yield from assert_scene_controller_works( + 'script#test', + 'script.turn_on', + None, + hass) + + +@asyncio.coroutine +def test_cancelable_script(hass): + """Test cancalable script discovery.""" + device = ( + 'script.test_2', + 'off', + {'friendly_name': "Test script 2", 'can_cancel': True}, + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'script#test_2' + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert capability['supportsDeactivation'] + + yield from assert_scene_controller_works( + 'script#test_2', + 'script.turn_on', + 'script.turn_off', + hass) + + +@asyncio.coroutine +def test_input_boolean(hass): + """Test input boolean discovery.""" + device = ( + 'input_boolean.test', + 'off', + {'friendly_name': "Test input boolean"}, + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'input_boolean#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test input boolean" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( + 'input_boolean#test', + 'input_boolean.turn_on', + 'input_boolean.turn_off', + hass) + + +@asyncio.coroutine +def test_scene(hass): + """Test scene discovery.""" + device = ('scene.test', 'off', {'friendly_name': "Test scene"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'scene#test' + assert appliance['displayCategories'][0] == "SCENE_TRIGGER" + assert appliance['friendlyName'] == "Test scene" + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert not capability['supportsDeactivation'] + + yield from assert_scene_controller_works( + 'scene#test', + 'scene.turn_on', + None, + hass) + + +@asyncio.coroutine +def test_fan(hass): + """Test fan discovery.""" + device = ('fan.test_1', 'off', {'friendly_name': "Test fan 1"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'fan#test_1' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 1" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + +@asyncio.coroutine +def test_variable_fan(hass): + """Test fan discovery. + + This one has variable speed. + """ + device = ( + 'fan.test_2', + 'off', { + 'friendly_name': "Test fan 2", + 'supported_features': 1, + 'speed_list': ['low', 'medium', 'high'], + 'speed': 'high', + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'fan#test_2' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 2" + + assert_endpoint_capabilities( + appliance, + 'Alexa.PercentageController', + 'Alexa.PowerController', + ) + + call, _ = yield from assert_request_calls_service( + 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2', + 'fan.set_speed', + hass, + payload={'percentage': '50'}) + assert call.data['speed'] == 'medium' + + yield from assert_percentage_changes( + hass, + [('high', '-5'), ('off', '5'), ('low', '-80')], + 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2', + 'percentageDelta', + 'fan.set_speed', + 'speed') + + +@asyncio.coroutine +def test_lock(hass): + """Test lock discovery.""" + device = ('lock.test', 'off', {'friendly_name': "Test lock"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'lock#test' + assert appliance['displayCategories'][0] == "SMARTLOCK" + assert appliance['friendlyName'] == "Test lock" + assert_endpoint_capabilities(appliance, 'Alexa.LockController') + + yield from assert_request_calls_service( + 'Alexa.LockController', 'Lock', 'lock#test', + 'lock.lock', + hass) + + +@asyncio.coroutine +def test_media_player(hass): + """Test media player discovery.""" + device = ( + 'media_player.test', + 'off', { + 'friendly_name': "Test media player", + 'supported_features': 0x59bd, + 'volume_level': 0.75 + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'media_player#test' + assert appliance['displayCategories'][0] == "TV" + assert appliance['friendlyName'] == "Test media player" + + assert_endpoint_capabilities( + appliance, + 'Alexa.InputController', + 'Alexa.PowerController', + 'Alexa.Speaker', + 'Alexa.StepSpeaker', + 'Alexa.PlaybackController', + ) + + yield from assert_power_controller_works( + 'media_player#test', + 'media_player.turn_on', + 'media_player.turn_off', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Play', 'media_player#test', + 'media_player.media_play', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Pause', 'media_player#test', + 'media_player.media_pause', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Stop', 'media_player#test', + 'media_player.media_stop', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Next', 'media_player#test', + 'media_player.media_next_track', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Previous', 'media_player#test', + 'media_player.media_previous_track', + hass) + + call, _ = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': 50}) + assert call.data['volume_level'] == 0.5 + + call, _ = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': True}) + assert call.data['is_volume_muted'] + + call, _, = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': False}) + assert not call.data['is_volume_muted'] + + yield from assert_percentage_changes( + hass, + [(0.7, '-5'), (0.8, '5'), (0, '-80')], + 'Alexa.Speaker', 'AdjustVolume', 'media_player#test', + 'volume', + 'media_player.volume_set', + 'volume_level') + + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': True}) + assert call.data['is_volume_muted'] + + call, _, = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': False}) + assert not call.data['is_volume_muted'] + + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': 20}) + assert call.data['volume_level'] == 0.95 + + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': -20}) + assert call.data['volume_level'] == 0.55 + + +@asyncio.coroutine +def test_alert(hass): + """Test alert discovery.""" + device = ('alert.test', 'off', {'friendly_name': "Test alert"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'alert#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test alert" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( + 'alert#test', + 'alert.turn_on', + 'alert.turn_off', + hass) + + +@asyncio.coroutine +def test_automation(hass): + """Test automation discovery.""" + device = ('automation.test', 'off', {'friendly_name': "Test automation"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'automation#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test automation" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( + 'automation#test', + 'automation.turn_on', + 'automation.turn_off', + hass) + + +@asyncio.coroutine +def test_group(hass): + """Test group discovery.""" + device = ('group.test', 'off', {'friendly_name': "Test group"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'group#test' + assert appliance['displayCategories'][0] == "SCENE_TRIGGER" + assert appliance['friendlyName'] == "Test group" + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert capability['supportsDeactivation'] + + yield from assert_scene_controller_works( + 'group#test', + 'homeassistant.turn_on', + 'homeassistant.turn_off', + hass) + + +@asyncio.coroutine +def test_cover(hass): + """Test cover discovery.""" + device = ( + 'cover.test', + 'off', { + 'friendly_name': "Test cover", + 'supported_features': 255, + 'position': 30, + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'cover#test' + assert appliance['displayCategories'][0] == "DOOR" + assert appliance['friendlyName'] == "Test cover" + + assert_endpoint_capabilities( + appliance, + 'Alexa.PercentageController', + 'Alexa.PowerController', + ) + + yield from assert_power_controller_works( + 'cover#test', + 'cover.open_cover', + 'cover.close_cover', + hass) + + call, _ = yield from assert_request_calls_service( + 'Alexa.PercentageController', 'SetPercentage', 'cover#test', + 'cover.set_cover_position', + hass, + payload={'percentage': '50'}) + assert call.data['position'] == 50 + + yield from assert_percentage_changes( + hass, + [(25, '-5'), (35, '5'), (0, '-80')], + 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test', + 'percentageDelta', + 'cover.set_cover_position', + 'position') + + +@asyncio.coroutine +def assert_percentage_changes( + hass, + adjustments, + namespace, + name, + endpoint, + parameter, + service, + changed_parameter): + """Assert an API request making percentage changes works. + + AdjustPercentage, AdjustBrightness, etc. are examples of such requests. + """ + for result_volume, adjustment in adjustments: + if parameter: + payload = {parameter: adjustment} + else: + payload = {} + + call, _ = yield from assert_request_calls_service( + namespace, name, endpoint, service, + hass, + payload=payload) + assert call.data[changed_parameter] == result_volume + + +@asyncio.coroutine +def test_temp_sensor(hass): + """Test temperature sensor discovery.""" + device = ( + 'sensor.test_temp', + '42', + { + 'friendly_name': "Test Temp Sensor", + 'unit_of_measurement': TEMP_FAHRENHEIT, + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'sensor#test_temp' + assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' + assert appliance['friendlyName'] == 'Test Temp Sensor' + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.TemperatureSensor') + assert capability['interface'] == 'Alexa.TemperatureSensor' + properties = capability['properties'] + assert properties['retrievable'] is True + assert {'name': 'temperature'} in properties['supported'] + + properties = yield from reported_properties(hass, 'sensor#test_temp') + properties.assert_equal('Alexa.TemperatureSensor', 'temperature', + {'value': 42.0, 'scale': 'FAHRENHEIT'}) + + +@asyncio.coroutine +def test_unknown_sensor(hass): + """Test sensors of unknown quantities are not discovered.""" + device = ( + 'sensor.test_sickness', '0.1', { + 'friendly_name': "Test Space Sickness Sensor", + 'unit_of_measurement': 'garn', + }) + yield from discovery_test(device, hass, expected_endpoints=0) @asyncio.coroutine @@ -440,7 +770,7 @@ def test_api_entity_not_exists(hass): assert 'event' in msg msg = msg['event'] - assert len(call_switch) == 0 + assert not call_switch assert msg['header']['name'] == 'ErrorResponse' assert msg['header']['namespace'] == 'Alexa' assert msg['payload']['type'] == 'NO_SUCH_ENDPOINT' @@ -462,102 +792,95 @@ def test_api_function_not_implemented(hass): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', - 'input_boolean', 'light', - 'switch']) -def test_api_turn_on(hass, domain): - """Test api turn on process.""" - request = get_new_request( - 'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain)) +def assert_request_fails( + namespace, + name, + endpoint, + service_not_called, + hass, + payload=None): + """Assert an API request returns an ErrorResponse.""" + request = get_new_request(namespace, name, endpoint) + if payload: + request['directive']['payload'] = payload - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call_domain = domain - - if domain == 'cover': - call = async_mock_service(hass, call_domain, 'open_cover') - else: - call = async_mock_service(hass, call_domain, 'turn_on') + domain, service_name = service_not_called.split('.') + call = async_mock_service(hass, domain, service_name) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) yield from hass.async_block_till_done() + assert not call assert 'event' in msg - msg = msg['event'] + assert msg['event']['header']['name'] == 'ErrorResponse' - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' + return msg @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group', - 'input_boolean', 'light', 'script', - 'switch']) -def test_api_turn_off(hass, domain): - """Test api turn on process.""" - request = get_new_request( - 'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain)) +def assert_request_calls_service( + namespace, + name, + endpoint, + service, + hass, + response_type='Response', + payload=None): + """Assert an API request calls a hass service.""" + request = get_new_request(namespace, name, endpoint) + if payload: + request['directive']['payload'] = payload - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'on', { - 'friendly_name': "Test {}".format(domain) - }) - - call_domain = domain - - if domain == 'group': - call_domain = 'homeassistant' - - if domain == 'cover': - call = async_mock_service(hass, call_domain, 'close_cover') - else: - call = async_mock_service(hass, call_domain, 'turn_off') + domain, service_name = service.split('.') + call = async_mock_service(hass, domain, service_name) 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' + assert 'event' in msg + assert call[0].data['entity_id'] == endpoint.replace('#', '.') + assert msg['event']['header']['name'] == response_type + + return call[0], msg @asyncio.coroutine -def test_api_set_brightness(hass): - """Test api set brightness process.""" - request = get_new_request( - 'Alexa.BrightnessController', 'SetBrightness', 'light#test') +def assert_power_controller_works(endpoint, on_service, off_service, hass): + """Assert PowerController API requests work.""" + yield from assert_request_calls_service( + 'Alexa.PowerController', 'TurnOn', endpoint, + on_service, hass) - # add payload - request['directive']['payload']['brightness'] = '50' + yield from assert_request_calls_service( + 'Alexa.PowerController', 'TurnOff', endpoint, + off_service, hass) - # setup test devices - hass.states.async_set( - 'light.test', 'off', {'friendly_name': "Test light"}) - call_light = async_mock_service(hass, 'light', 'turn_on') +@asyncio.coroutine +def assert_scene_controller_works( + endpoint, + activate_service, + deactivate_service, + hass): + """Assert SceneController API requests work.""" + _, response = yield from assert_request_calls_service( + 'Alexa.SceneController', 'Activate', endpoint, + activate_service, hass, + response_type='ActivationStarted') + assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION' + assert 'timestamp' in response['event']['payload'] - 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['brightness_pct'] == 50 - assert msg['header']['name'] == 'Response' + if deactivate_service: + yield from assert_request_calls_service( + 'Alexa.SceneController', 'Deactivate', endpoint, + deactivate_service, hass, + response_type='DeactivationStarted') + cause_type = response['event']['payload']['cause']['type'] + assert cause_type == 'VOICE_INTERACTION' + assert 'timestamp' in response['event']['payload'] @asyncio.coroutine @@ -753,475 +1076,55 @@ def test_api_increase_color_temp(hass, result, initial): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['scene', 'group', 'script']) -def test_api_activate(hass, domain): - """Test api activate process.""" - request = get_new_request( - 'Alexa.SceneController', 'Activate', '{}#test'.format(domain)) - - # setup test devices +def test_report_lock_state(hass): + """Test LockController implements lockState property.""" hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) + 'lock.locked', STATE_LOCKED, {}) + hass.states.async_set( + 'lock.unlocked', STATE_UNLOCKED, {}) + hass.states.async_set( + 'lock.unknown', STATE_UNKNOWN, {}) - if domain == 'group': - call_domain = 'homeassistant' - else: - call_domain = domain + properties = yield from reported_properties(hass, 'lock.locked') + properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED') - call = async_mock_service(hass, call_domain, 'turn_on') + properties = yield from reported_properties(hass, 'lock.unlocked') + properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED') - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'ActivationStarted' - assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION' - assert 'timestamp' in msg['payload'] + properties = yield from reported_properties(hass, 'lock.unknown') + properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED') @asyncio.coroutine -@pytest.mark.parametrize("domain", ['group', 'script']) -def test_api_deactivate(hass, domain): - """Test api deactivate process.""" - request = get_new_request( - 'Alexa.SceneController', 'Deactivate', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - if domain == 'group': - call_domain = 'homeassistant' - else: - call_domain = domain - - call = async_mock_service(hass, call_domain, 'turn_off') +def reported_properties(hass, endpoint): + """Use ReportState to get properties and return them. + The result is a _ReportedProperties instance, which has methods to make + assertions about the properties. + """ + request = get_new_request('Alexa', 'ReportState', endpoint) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) yield from hass.async_block_till_done() + return _ReportedProperties(msg['context']['properties']) - assert 'event' in msg - msg = msg['event'] - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'DeactivationStarted' - assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION' - assert 'timestamp' in msg['payload'] +class _ReportedProperties(object): + def __init__(self, properties): + self.properties = properties + def assert_equal(self, namespace, name, value): + """Assert a property is equal to a given value.""" + for prop in self.properties: + if prop['namespace'] == namespace and prop['name'] == name: + assert prop['value'] == value + return prop -@asyncio.coroutine -def test_api_set_percentage_fan(hass): - """Test api set percentage for fan process.""" - request = get_new_request( - 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2') - - # add payload - request['directive']['payload']['percentage'] = '50' - - # setup test devices - hass.states.async_set( - 'fan.test_2', 'off', {'friendly_name': "Test fan"}) - - call_fan = async_mock_service(hass, 'fan', 'set_speed') - - 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_fan) == 1 - assert call_fan[0].data['entity_id'] == 'fan.test_2' - assert call_fan[0].data['speed'] == 'medium' - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_set_percentage_cover(hass): - """Test api set percentage for cover process.""" - request = get_new_request( - 'Alexa.PercentageController', 'SetPercentage', 'cover#test') - - # add payload - request['directive']['payload']['percentage'] = '50' - - # setup test devices - hass.states.async_set( - 'cover.test', 'closed', { - 'friendly_name': "Test cover" - }) - - call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - - 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_cover) == 1 - assert call_cover[0].data['entity_id'] == 'cover.test' - assert call_cover[0].data['position'] == 50 - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')]) -def test_api_adjust_percentage_fan(hass, result, adjust): - """Test api adjust percentage for fan process.""" - request = get_new_request( - 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2') - - # add payload - request['directive']['payload']['percentageDelta'] = adjust - - # setup test devices - hass.states.async_set( - 'fan.test_2', 'on', { - 'friendly_name': "Test fan 2", 'speed': 'high' - }) - - call_fan = async_mock_service(hass, 'fan', 'set_speed') - - 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_fan) == 1 - assert call_fan[0].data['entity_id'] == 'fan.test_2' - assert call_fan[0].data['speed'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')]) -def test_api_adjust_percentage_cover(hass, result, adjust): - """Test api adjust percentage for cover process.""" - request = get_new_request( - 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test') - - # add payload - request['directive']['payload']['percentageDelta'] = adjust - - # setup test devices - hass.states.async_set( - 'cover.test', 'closed', { - 'friendly_name': "Test cover", - 'position': 30 - }) - - call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - - 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_cover) == 1 - assert call_cover[0].data['entity_id'] == 'cover.test' - assert call_cover[0].data['position'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['lock']) -def test_api_lock(hass, domain): - """Test api lock process.""" - request = get_new_request( - 'Alexa.LockController', 'Lock', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'lock') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_play(hass, domain): - """Test api play process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_play') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_pause(hass, domain): - """Test api pause process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_pause') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_stop(hass, domain): - """Test api stop process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_stop') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_next(hass, domain): - """Test api next process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_next_track') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_previous(hass, domain): - """Test api previous process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_previous_track') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_set_volume(hass): - """Test api set volume process.""" - request = get_new_request( - 'Alexa.Speaker', 'SetVolume', 'media_player#test') - - # add payload - request['directive']['payload']['volume'] = 50 - - # setup test devices - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", 'volume_level': 0 - }) - - call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - - 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_media_player) == 1 - assert call_media_player[0].data['entity_id'] == 'media_player.test' - assert call_media_player[0].data['volume_level'] == 0.5 - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')]) -def test_api_adjust_volume(hass, result, adjust): - """Test api adjust volume process.""" - request = get_new_request( - 'Alexa.Speaker', 'AdjustVolume', 'media_player#test') - - # add payload - request['directive']['payload']['volume'] = adjust - - # setup test devices - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", 'volume_level': 0.75 - }) - - call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - - 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_media_player) == 1 - assert call_media_player[0].data['entity_id'] == 'media_player.test' - assert call_media_player[0].data['volume_level'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_mute(hass, domain): - """Test api mute process.""" - request = get_new_request( - 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain)) - - request['directive']['payload']['mute'] = True - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'volume_mute') - - 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) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_report_temperature(hass): - """Test API ReportState response for a temperature sensor.""" - request = get_new_request('Alexa', 'ReportState', 'sensor#test') - - # setup test devices - hass.states.async_set( - 'sensor.test', '42', { - 'friendly_name': 'test sensor', - CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, - }) - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - header = msg['event']['header'] - assert header['namespace'] == 'Alexa' - assert header['name'] == 'StateReport' - - properties = msg['context']['properties'] - assert len(properties) == 1 - prop = properties[0] - assert prop['namespace'] == 'Alexa.TemperatureSensor' - assert prop['name'] == 'temperature' - assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} + assert False, 'property %s:%s not in %r' % ( + namespace, + name, + self.properties, + ) @asyncio.coroutine @@ -1275,7 +1178,7 @@ def test_unsupported_domain(hass): assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 0 + assert not msg['payload']['endpoints'] @asyncio.coroutine @@ -1318,3 +1221,38 @@ def test_http_api_disabled(hass, test_client): response = yield from do_http_discovery(config, hass, test_client) assert response.status == 404 + + +@asyncio.coroutine +@pytest.mark.parametrize( + "domain,payload,source_list,idx", [ + ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1), + ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0), + ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0), + ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None), + ] +) +def test_api_select_input(hass, domain, payload, source_list, idx): + """Test api set input process.""" + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", + 'source': 'unknown', + 'source_list': source_list, + }) + + # test where no source matches + if idx is None: + yield from assert_request_fails( + 'Alexa.InputController', 'SelectInput', 'media_player#test', + 'media_player.select_source', + hass, + payload={'input': payload}) + return + + call, _ = yield from assert_request_calls_service( + 'Alexa.InputController', 'SelectInput', 'media_player#test', + 'media_player.select_source', + hass, + payload={'input': payload}) + assert call.data['source'] == source_list[idx] diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 58cfd2cbd70..63ca4b5cd1a 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -736,7 +736,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_not_fires_on_entities_change_with_for_afte_stop(self): + def test_if_not_fires_on_entities_change_with_for_after_stop(self): """Test for not firing on entities change with for after stop.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index bf54d24492a..22c84b88935 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -198,7 +198,7 @@ class TestAutomationState(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'state', - 'entity_id': 'test.anoter_entity', + 'entity_id': 'test.another_entity', }, 'action': { 'service': 'test.automation' @@ -468,7 +468,7 @@ class TestAutomationState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_for_condition(self): - """Test for firing if contition is on.""" + """Test for firing if condition is on.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: @@ -504,7 +504,7 @@ class TestAutomationState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_for_condition_attribute_change(self): - """Test for firing if contition is on with attribute change.""" + """Test for firing if condition is on with attribute change.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=4) point3 = point1 + timedelta(seconds=8) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index ac1d7bc5acf..355d088719f 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -127,7 +127,7 @@ class TestAutomationSun(unittest.TestCase): self.assertEqual('sun - sunset - 0:30:00', self.calls[0].data['some']) def test_sunrise_trigger_with_offset(self): - """Test the runrise trigger with offset.""" + """Test the sunrise trigger with offset.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) diff --git a/tests/components/binary_sensor/test_aurora.py b/tests/components/binary_sensor/test_aurora.py index ed68d23905f..1198aeb1357 100644 --- a/tests/components/binary_sensor/test_aurora.py +++ b/tests/components/binary_sensor/test_aurora.py @@ -28,7 +28,7 @@ class TestAuroraSensorSetUp(unittest.TestCase): def test_setup_and_initial_state(self, mock_req): """Test that the component is created and initialized as expected.""" uri = re.compile( - "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" + r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" ) mock_req.get(uri, text=load_fixture('aurora.txt')) @@ -66,7 +66,7 @@ class TestAuroraSensorSetUp(unittest.TestCase): def test_custom_threshold_works(self, mock_req): """Test that the config can take a custom forecast threshold.""" uri = re.compile( - "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" + r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" ) mock_req.get(uri, text=load_fixture('aurora.txt')) diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 396020561ac..9b5cf7aa736 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,13 +1,15 @@ """The tests for the MQTT binary sensor platform.""" import unittest +import homeassistant.core as ha from homeassistant.setup import setup_component import homeassistant.components.binary_sensor as binary_sensor -from homeassistant.const import (STATE_OFF, STATE_ON, - STATE_UNAVAILABLE) -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE + +from tests.common import get_test_home_assistant, fire_mqtt_message +from tests.common import mock_component, mock_mqtt_component class TestSensorMQTT(unittest.TestCase): @@ -141,3 +143,64 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_force_update_disabled(self): + """Test force update option.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF' + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + def test_force_update_enabled(self): + """Test force update option.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'force_update': True + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(2, len(events)) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 481226c4f73..c47f23bf902 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -98,13 +98,75 @@ class TestBinarySensorTemplate(unittest.TestCase): } }) + def test_icon_template(self): + """Test icon template.""" + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "State", + 'icon_template': + "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes.get('icon') == '' + + self.hass.states.set('binary_sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes['icon'] == 'mdi:check' + + def test_entity_picture_template(self): + """Test entity_picture template.""" + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "State", + 'entity_picture_template': + "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "/local/sensor.png" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes.get('entity_picture') == '' + + self.hass.states.set('binary_sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes['entity_picture'] == '/local/sensor.png' + def test_attributes(self): """"Test the attributes.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', - template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL, - None, None + template_hlpr.Template('{{ 1 > 1 }}', self.hass), + None, None, MATCH_ALL, None, None ).result() self.assertFalse(vs.should_poll) self.assertEqual('motion', vs.device_class) @@ -156,8 +218,8 @@ class TestBinarySensorTemplate(unittest.TestCase): vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', - template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL, - None, None + template_hlpr.Template('{{ 1 > 1 }}', self.hass), + None, None, MATCH_ALL, None, None ).result() mock_render.side_effect = TemplateError('foo') run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py index 38573b295d3..926b3c67983 100644 --- a/tests/components/binary_sensor/test_threshold.py +++ b/tests/components/binary_sensor/test_threshold.py @@ -333,3 +333,63 @@ class TestThresholdSensor(unittest.TestCase): self.assertEqual('unknown', state.attributes.get('position')) assert state.state == 'off' + + def test_sensor_lower_zero_threshold(self): + """Test if a lower threshold of zero is set.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'lower': '0', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('lower', state.attributes.get('type')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', -3) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'on' + + def test_sensor_upper_zero_threshold(self): + """Test if an upper threshold of zero is set.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'upper': '0', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', -10) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('upper', state.attributes.get('type')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 2) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'on' diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py index 91d5da34901..a13944aef9f 100644 --- a/tests/components/binary_sensor/test_vultr.py +++ b/tests/components/binary_sensor/test_vultr.py @@ -78,7 +78,7 @@ class TestVultrBinarySensorSetup(unittest.TestCase): for device in self.DEVICES: - # Test pre data retieval + # Test pre data retrieval if device.subscription == '555555': self.assertEqual('Vultr {}', device.name) diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 1de825efd99..62c8ea8854f 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -1,423 +1,423 @@ -"""The tests for the google calendar component.""" -# pylint: disable=protected-access -import logging -import unittest -from unittest.mock import patch - -import pytest - -import homeassistant.components.calendar as calendar_base -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 - -TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} - -_LOGGER = logging.getLogger(__name__) - - -class TestComponentsGoogleCalendar(unittest.TestCase): - """Test the Google calendar.""" - - hass = None # HomeAssistant - - # pylint: disable=invalid-name - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina')) - - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - dt_util.set_default_time_zone(dt_util.get_time_zone('UTC')) - - self.hass.stop() - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - week_from_today = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=7) - event = { - 'summary': 'Test All Day Event', - 'start': { - 'date': week_from_today.isoformat() - }, - 'end': { - 'date': (week_from_today + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, - '', {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': True, - 'offset_reached': False, - 'start_time': '{} 00:00:00'.format(event['start']['date']), - 'end_time': '{} 00:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_future_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - one_hour_from_now = dt_util.now() \ - + dt_util.dt.timedelta(minutes=30) - event = { - 'start': { - 'dateTime': one_hour_from_now.isoformat() - }, - 'end': { - 'dateTime': (one_hour_from_now - + dt_util.dt.timedelta(minutes=60)) - .isoformat() - }, - 'summary': 'Test Event in 30 minutes', - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - mock_next_event.return_value.event = event - - device_name = 'Test Future Event' - device_id = 'test_future_event' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': False, - 'offset_reached': False, - 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT), - 'end_time': - (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() \ - - dt_util.dt.timedelta(minutes=30) - event = { - 'start': { - 'dateTime': middle_of_event.isoformat() - }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt - .timedelta(minutes=60)) - .isoformat() - }, - 'summary': 'Test Event in Progress', - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - - mock_next_event.return_value.event = event - - device_name = 'Test Event in Progress' - device_id = 'test_event_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_ON) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': False, - 'offset_reached': False, - 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), - 'end_time': - (middle_of_event + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_offset_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() \ - + dt_util.dt.timedelta(minutes=14) - event_summary = 'Test Event in Progress' - event = { - 'start': { - 'dateTime': middle_of_event.isoformat() - }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt - .timedelta(minutes=60)) - .isoformat() - }, - 'summary': '{} !!-15'.format(event_summary), - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - - mock_next_event.return_value.event = event - - device_name = 'Test Event in Progress' - device_id = 'test_event_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertTrue(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': False, - 'offset_reached': True, - 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), - 'end_time': - (middle_of_event + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @pytest.mark.skip - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_offset_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=1) - - event_summary = 'Test All Day Event Offset In Progress' - event = { - 'summary': '{} !!-25:0'.format(event_summary), - 'start': { - 'date': tomorrow.isoformat() - }, - 'end': { - 'date': (tomorrow + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day Offset In Progress' - device_id = 'test_all_day_offset_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertTrue(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': True, - 'offset_reached': True, - 'start_time': '{} 06:00:00'.format(event['start']['date']), - 'end_time': '{} 06:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_offset_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=2) - - offset_hours = (1 + dt_util.now().hour) - event_summary = 'Test All Day Event Offset' - event = { - 'summary': '{} !!-{}:0'.format(event_summary, offset_hours), - 'start': { - 'date': tomorrow.isoformat() - }, - 'end': { - 'date': (tomorrow + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day Offset' - device_id = 'test_all_day_offset' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': True, - 'offset_reached': False, - 'start_time': '{} 00:00:00'.format(event['start']['date']), - 'end_time': '{} 00:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) +"""The tests for the google calendar component.""" +# pylint: disable=protected-access +import logging +import unittest +from unittest.mock import patch + +import pytest + +import homeassistant.components.calendar as calendar_base +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 + +TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} + +_LOGGER = logging.getLogger(__name__) + + +class TestComponentsGoogleCalendar(unittest.TestCase): + """Test the Google calendar.""" + + hass = None # HomeAssistant + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina')) + + # pylint: disable=invalid-name + def tearDown(self): + """Stop everything that was started.""" + dt_util.set_default_time_zone(dt_util.get_time_zone('UTC')) + + self.hass.stop() + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + week_from_today = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=7) + event = { + 'summary': 'Test All Day Event', + 'start': { + 'date': week_from_today.isoformat() + }, + 'end': { + 'date': (week_from_today + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, + '', {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': True, + 'offset_reached': False, + 'start_time': '{} 00:00:00'.format(event['start']['date']), + 'end_time': '{} 00:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_future_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + one_hour_from_now = dt_util.now() \ + + dt_util.dt.timedelta(minutes=30) + event = { + 'start': { + 'dateTime': one_hour_from_now.isoformat() + }, + 'end': { + 'dateTime': (one_hour_from_now + + dt_util.dt.timedelta(minutes=60)) + .isoformat() + }, + 'summary': 'Test Event in 30 minutes', + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + mock_next_event.return_value.event = event + + device_name = 'Test Future Event' + device_id = 'test_future_event' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': False, + 'offset_reached': False, + 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT), + 'end_time': + (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + middle_of_event = dt_util.now() \ + - dt_util.dt.timedelta(minutes=30) + event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt + .timedelta(minutes=60)) + .isoformat() + }, + 'summary': 'Test Event in Progress', + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + + mock_next_event.return_value.event = event + + device_name = 'Test Event in Progress' + device_id = 'test_event_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_ON) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': False, + 'offset_reached': False, + 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), + 'end_time': + (middle_of_event + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_offset_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + middle_of_event = dt_util.now() \ + + dt_util.dt.timedelta(minutes=14) + event_summary = 'Test Event in Progress' + event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt + .timedelta(minutes=60)) + .isoformat() + }, + 'summary': '{} !!-15'.format(event_summary), + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + + mock_next_event.return_value.event = event + + device_name = 'Test Event in Progress' + device_id = 'test_event_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertTrue(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': False, + 'offset_reached': True, + 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), + 'end_time': + (middle_of_event + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @pytest.mark.skip + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_offset_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + tomorrow = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=1) + + event_summary = 'Test All Day Event Offset In Progress' + event = { + 'summary': '{} !!-25:0'.format(event_summary), + 'start': { + 'date': tomorrow.isoformat() + }, + 'end': { + 'date': (tomorrow + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day Offset In Progress' + device_id = 'test_all_day_offset_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertTrue(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': True, + 'offset_reached': True, + 'start_time': '{} 06:00:00'.format(event['start']['date']), + 'end_time': '{} 06:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_offset_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + tomorrow = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=2) + + offset_hours = (1 + dt_util.now().hour) + event_summary = 'Test All Day Event Offset' + event = { + 'summary': '{} !!-{}:0'.format(event_summary, offset_hours), + 'start': { + 'date': tomorrow.isoformat() + }, + 'end': { + 'date': (tomorrow + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day Offset' + device_id = 'test_all_day_offset' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': True, + 'offset_reached': False, + 'start_time': '{} 00:00:00'.format(event['start']['date']), + 'end_time': '{} 00:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 70e95dd7b93..87612da9faa 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -83,7 +83,7 @@ class TestGetImage(object): @patch('homeassistant.components.camera.demo.DemoCamera.camera_image', autospec=True, return_value=b'Test') def test_get_image_from_camera(self, mock_camera): - """Grab a image from camera entity.""" + """Grab an image from camera entity.""" self.hass.start() image = run_coroutine_threadsafe(camera.async_get_image( diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 812dd399a48..42ce7bd7add 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -8,10 +8,14 @@ from mock_open import MockOpen from homeassistant.setup import async_setup_component +from tests.common import mock_registry + @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" + mock_registry(hass) + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ mock.patch('os.access', mock.Mock(return_value=True)): yield from async_setup_component(hass, 'camera', { diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 9098494bf48..b2633a75583 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -224,10 +224,10 @@ class TestDemoClimate(unittest.TestCase): def test_set_hold_mode_none(self): """Test setting the hold mode off/false.""" - climate.set_hold_mode(self.hass, None, ENTITY_ECOBEE) + climate.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) - self.assertEqual(None, state.attributes.get('hold_mode')) + self.assertEqual('off', state.attributes.get('hold_mode')) def test_set_aux_heat_bad_attr(self): """Test setting the auxiliary heater without required attribute.""" diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 190eb7e8522..abc9e6d74c2 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, STATE_OFF, + STATE_IDLE, TEMP_CELSIUS, ATTR_TEMPERATURE ) @@ -170,7 +171,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_setup_defaults_to_unknown(self): """Test the setting of defaults to unknown.""" - self.assertEqual('idle', self.hass.states.get(ENTITY).state) + self.assertEqual(STATE_IDLE, self.hass.states.get(ENTITY).state) def test_default_setup_params(self): """Test the setup with default parameters.""" @@ -964,6 +965,7 @@ def test_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 20) assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off") + assert(state.state == STATE_OFF) @asyncio.coroutine @@ -990,3 +992,84 @@ def test_no_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 22) + assert(state.state == STATE_OFF) + + +class TestClimateGenericThermostatRestoreState(unittest.TestCase): + """Test generic thermostat when restore state from HA startup.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.temperature_unit = TEMP_CELSIUS + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_restore_state_uncoherence_case(self): + """ + Test restore from a strange state. + + - Turn the generic thermostat off + - Restart HA and restore state from DB + """ + self._mock_restore_cache(temperature=20) + + self._setup_switch(False) + self._setup_sensor(15) + self._setup_climate() + self.hass.block_till_done() + + state = self.hass.states.get(ENTITY) + self.assertEqual(20, state.attributes[ATTR_TEMPERATURE]) + self.assertEqual(STATE_OFF, + state.attributes[climate.ATTR_OPERATION_MODE]) + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(0, len(self.calls)) + + self._setup_switch(False) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(STATE_OFF, + state.attributes[climate.ATTR_OPERATION_MODE]) + self.assertEqual(STATE_OFF, state.state) + + def _setup_climate(self): + assert setup_component(self.hass, climate.DOMAIN, {'climate': { + 'platform': 'generic_thermostat', + 'name': 'test', + 'cold_tolerance': 2, + 'hot_tolerance': 4, + 'away_temp': 30, + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR, + 'ac_mode': True + }}) + + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): + """Setup the test sensor.""" + self.hass.states.set(ENT_SENSOR, temp, { + ATTR_UNIT_OF_MEASUREMENT: unit + }) + + def _setup_switch(self, is_on): + """Setup the test switch.""" + self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + self.calls = [] + + @callback + def log_call(call): + """Log service calls.""" + self.calls.append(call) + + self.hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, log_call) + self.hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, log_call) + + def _mock_restore_cache(self, temperature=20, operation_mode=STATE_OFF): + mock_restore_cache(self.hass, ( + State(ENTITY, '0', { + ATTR_TEMPERATURE: str(temperature), + climate.ATTR_OPERATION_MODE: operation_mode, + ATTR_AWAY_MODE: "on"}), + )) diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py new file mode 100644 index 00000000000..f8a044c2f4b --- /dev/null +++ b/tests/components/climate/test_melissa.py @@ -0,0 +1,267 @@ +"""Test for Melissa climate component.""" +import unittest +from unittest.mock import Mock, patch +import json + +from asynctest import mock + +from homeassistant.components.climate import ( + melissa, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + SUPPORT_ON_OFF, SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, + STATE_COOL, STATE_AUTO +) +from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, ATTR_TEMPERATURE, STATE_OFF, STATE_IDLE +) +from tests.common import get_test_home_assistant, load_fixture + + +class TestMelissa(unittest.TestCase): + """Tests for Melissa climate.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up test variables.""" + self.hass = get_test_home_assistant() + self._serial = '12345678' + + self.api = Mock() + self.api.fetch_devices.return_value = json.loads(load_fixture( + 'melissa_fetch_devices.json' + )) + self.api.cur_settings.return_value = json.loads(load_fixture( + 'melissa_cur_settings.json' + )) + self.api.status.return_value = json.loads(load_fixture( + 'melissa_status.json' + )) + self.api.STATE_OFF = 0 + self.api.STATE_ON = 1 + self.api.STATE_IDLE = 2 + + self.api.MODE_AUTO = 0 + self.api.MODE_FAN = 1 + self.api.MODE_HEAT = 2 + self.api.MODE_COOL = 3 + self.api.MODE_DRY = 4 + + self.api.FAN_AUTO = 0 + self.api.FAN_LOW = 1 + self.api.FAN_MEDIUM = 2 + self.api.FAN_HIGH = 3 + + self.api.STATE = 'state' + self.api.MODE = 'mode' + self.api.FAN = 'fan' + self.api.TEMP = 'temp' + + device = self.api.fetch_devices()[self._serial] + self.thermostat = melissa.MelissaClimate( + self.api, device['serial_number'], device) + self.thermostat.update() + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @patch("homeassistant.components.climate.melissa.MelissaClimate") + def test_setup_platform(self, mocked_thermostat): + """Test setup_platform.""" + device = self.api.fetch_devices()[self._serial] + thermostat = mocked_thermostat(self.api, device['serial_number'], + device) + thermostats = [thermostat] + + self.hass.data[DATA_MELISSA] = self.api + + config = {} + add_devices = Mock() + discovery_info = {} + + melissa.setup_platform(self.hass, config, add_devices, discovery_info) + add_devices.assert_called_once_with(thermostats) + + def test_get_name(self): + """Test name property.""" + self.assertEqual("Melissa 12345678", self.thermostat.name) + + def test_is_on(self): + """Test name property.""" + self.assertTrue(self.thermostat.is_on) + self.thermostat._cur_settings = None + self.assertFalse(self.thermostat.is_on) + + def test_current_fan_mode(self): + """Test current_fan_mode property.""" + self.thermostat.update() + self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + self.thermostat._cur_settings = None + self.assertEqual(None, self.thermostat.current_fan_mode) + + def test_current_temperature(self): + """Test current temperature.""" + self.assertEqual(27.4, self.thermostat.current_temperature) + + def test_current_temperature_no_data(self): + """Test current temperature without data.""" + self.thermostat._data = None + self.assertIsNone(self.thermostat.current_temperature) + + def test_target_temperature_step(self): + """Test current target_temperature_step.""" + self.assertEqual(1, self.thermostat.target_temperature_step) + + def test_current_operation(self): + """Test current operation.""" + self.thermostat.update() + self.assertEqual(self.thermostat.current_operation, STATE_HEAT) + self.thermostat._cur_settings = None + self.assertEqual(None, self.thermostat.current_operation) + + def test_operation_list(self): + """Test the operation list.""" + self.assertEqual( + [STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT], + self.thermostat.operation_list + ) + + def test_fan_list(self): + """Test the fan list.""" + self.assertEqual( + [STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM], + self.thermostat.fan_list + ) + + def test_target_temperature(self): + """Test target temperature.""" + self.assertEqual(16, self.thermostat.target_temperature) + self.thermostat._cur_settings = None + self.assertEqual(None, self.thermostat.target_temperature) + + def test_state(self): + """Test state.""" + self.assertEqual(STATE_ON, self.thermostat.state) + self.thermostat._cur_settings = None + self.assertEqual(None, self.thermostat.state) + + def test_temperature_unit(self): + """Test temperature unit.""" + self.assertEqual(TEMP_CELSIUS, self.thermostat.temperature_unit) + + def test_min_temp(self): + """Test min temp.""" + self.assertEqual(16, self.thermostat.min_temp) + + def test_max_temp(self): + """Test max temp.""" + self.assertEqual(30, self.thermostat.max_temp) + + def test_supported_features(self): + """Test supported_features property.""" + features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_ON_OFF | SUPPORT_FAN_MODE) + self.assertEqual(features, self.thermostat.supported_features) + + def test_set_temperature(self): + """Test set_temperature.""" + self.api.send.return_value = True + self.thermostat.update() + self.thermostat.set_temperature(**{ATTR_TEMPERATURE: 25}) + self.assertEqual(25, self.thermostat.target_temperature) + + def test_fan_mode(self): + """Test set_fan_mode.""" + self.api.send.return_value = True + self.thermostat.set_fan_mode(SPEED_HIGH) + self.assertEqual(SPEED_HIGH, self.thermostat.current_fan_mode) + + def test_set_operation_mode(self): + """Test set_operation_mode.""" + self.api.send.return_value = True + self.thermostat.set_operation_mode(STATE_COOL) + self.assertEqual(STATE_COOL, self.thermostat.current_operation) + + def test_turn_on(self): + """Test turn_on.""" + self.thermostat.turn_on() + self.assertTrue(self.thermostat.state) + + def test_turn_off(self): + """Test turn_off.""" + self.thermostat.turn_off() + self.assertEqual(STATE_OFF, self.thermostat.state) + + def test_send(self): + """Test send.""" + self.thermostat.update() + self.assertTrue(self.thermostat.send( + {'fan': self.api.FAN_MEDIUM})) + self.assertEqual(SPEED_MEDIUM, self.thermostat.current_fan_mode) + self.api.send.return_value = False + self.thermostat._cur_settings = None + self.assertFalse(self.thermostat.send({ + 'fan': self.api.FAN_LOW})) + self.assertNotEquals(SPEED_LOW, self.thermostat.current_fan_mode) + self.assertIsNone(self.thermostat._cur_settings) + + @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') + def test_update(self, mocked_warning): + """Test update.""" + self.thermostat.update() + self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + self.assertEqual(STATE_HEAT, self.thermostat.current_operation) + self.thermostat._api.status.side_effect = KeyError('boom') + self.thermostat.update() + mocked_warning.assert_called_once_with( + 'Unable to update entity %s', self.thermostat.entity_id) + + def test_melissa_state_to_hass(self): + """Test for translate melissa states to hass.""" + self.assertEqual(STATE_OFF, self.thermostat.melissa_state_to_hass(0)) + self.assertEqual(STATE_ON, self.thermostat.melissa_state_to_hass(1)) + self.assertEqual(STATE_IDLE, self.thermostat.melissa_state_to_hass(2)) + self.assertEqual(None, + self.thermostat.melissa_state_to_hass(3)) + + def test_melissa_op_to_hass(self): + """Test for translate melissa operations to hass.""" + self.assertEqual(STATE_AUTO, self.thermostat.melissa_op_to_hass(0)) + self.assertEqual(STATE_FAN_ONLY, self.thermostat.melissa_op_to_hass(1)) + self.assertEqual(STATE_HEAT, self.thermostat.melissa_op_to_hass(2)) + self.assertEqual(STATE_COOL, self.thermostat.melissa_op_to_hass(3)) + self.assertEqual(STATE_DRY, self.thermostat.melissa_op_to_hass(4)) + self.assertEqual( + None, self.thermostat.melissa_op_to_hass(5)) + + def test_melissa_fan_to_hass(self): + """Test for translate melissa fan state to hass.""" + self.assertEqual(STATE_AUTO, self.thermostat.melissa_fan_to_hass(0)) + self.assertEqual(SPEED_LOW, self.thermostat.melissa_fan_to_hass(1)) + self.assertEqual(SPEED_MEDIUM, self.thermostat.melissa_fan_to_hass(2)) + self.assertEqual(SPEED_HIGH, self.thermostat.melissa_fan_to_hass(3)) + self.assertEqual(None, self.thermostat.melissa_fan_to_hass(4)) + + @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') + def test_hass_mode_to_melissa(self, mocked_warning): + """Test for hass operations to melssa.""" + self.assertEqual(0, self.thermostat.hass_mode_to_melissa(STATE_AUTO)) + self.assertEqual( + 1, self.thermostat.hass_mode_to_melissa(STATE_FAN_ONLY)) + self.assertEqual(2, self.thermostat.hass_mode_to_melissa(STATE_HEAT)) + self.assertEqual(3, self.thermostat.hass_mode_to_melissa(STATE_COOL)) + self.assertEqual(4, self.thermostat.hass_mode_to_melissa(STATE_DRY)) + self.thermostat.hass_mode_to_melissa("test") + mocked_warning.assert_called_once_with( + "Melissa have no setting for %s mode", "test") + + @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') + def test_hass_fan_to_melissa(self, mocked_warning): + """Test for translate melissa states to hass.""" + self.assertEqual(0, self.thermostat.hass_fan_to_melissa(STATE_AUTO)) + self.assertEqual(1, self.thermostat.hass_fan_to_melissa(SPEED_LOW)) + self.assertEqual(2, self.thermostat.hass_fan_to_melissa(SPEED_MEDIUM)) + self.assertEqual(3, self.thermostat.hass_fan_to_melissa(SPEED_HIGH)) + self.thermostat.hass_fan_to_melissa("test") + mocked_warning.assert_called_once_with( + "Melissa have no setting for %s fan mode", "test") diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 707e49f670f..7a4e9f2950e 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1 +1 @@ -"""Tests for the cloud component.""" +"""Tests for the cloud component.""" diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 7623b25d401..69cd540e7d5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,8 +14,8 @@ from tests.common import mock_coro @pytest.fixture def cloud_client(hass, test_client): """Fixture that can fetch from the cloud client.""" - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): hass.loop.run_until_complete(async_setup_component(hass, 'cloud', { 'cloud': { 'mode': 'development', diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 7d23d9faad4..70990519a0b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -87,7 +87,7 @@ def test_initialize_loads_info(mock_os, hass): with patch('homeassistant.components.cloud.open', mopen, create=True), \ patch('homeassistant.components.cloud.Cloud._decode_claims'): - cl._start_cloud(None) + yield from cl.async_start(None) assert cl.id_token == 'test-id-token' assert cl.access_token == 'test-access-token' diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 529559f56af..53340ecede1 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -266,8 +266,8 @@ def test_handler_alexa(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'alexa': { @@ -309,8 +309,8 @@ def test_handler_google_actions(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'google_actions': { diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index af114135da9..3d7aa3ce618 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -275,7 +275,7 @@ class TestTemplateCover(unittest.TestCase): assert self.hass.states.all() == [] def test_template_open_and_close(self): - """Test that if open_cover is specified, cose_cover is too.""" + """Test that if open_cover is specified, close_cover is too.""" with assert_setup_component(0, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 6e646e9862d..f8d3fdf128b 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -378,7 +378,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): telnet.login.assert_not_called() def test_get_asuswrt_data(self): - """Test aususwrt data fetch.""" + """Test asuswrt data fetch.""" scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) scanner._get_wl = mock.Mock() scanner._get_arp = mock.Mock() diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 78813d9ff0b..84cca1bb843 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -704,7 +704,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): @asyncio.coroutine def test_async_added_to_hass(hass): - """Test resoring state.""" + """Test restoring state.""" attr = { device_tracker.ATTR_LONGITUDE: 18, device_tracker.ATTR_LATITUDE: -33, diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index eb461062971..78750e91f83 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -56,7 +56,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): def test_new_message(self): """Test new message.""" dev_id = 'paulus' - enttiy_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) topic = '/location/paulus' location = 'work' @@ -69,4 +69,80 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): }) fire_mqtt_message(self.hass, topic, location) self.hass.block_till_done() - self.assertEqual(location, self.hass.states.get(enttiy_id).state) + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/somewhere/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) diff --git a/tests/components/device_tracker/test_mqtt_json.py b/tests/components/device_tracker/test_mqtt_json.py index 1755f424d29..43f4fc3bbf3 100644 --- a/tests/components/device_tracker/test_mqtt_json.py +++ b/tests/components/device_tracker/test_mqtt_json.py @@ -123,3 +123,77 @@ class TestComponentsDeviceTrackerJSONMQTT(unittest.TestCase): "Skipping update for following data because of missing " "or malformatted data: {\"longitude\": 2.0}", test_handle.output[0]) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/+/zanzito' + topic = 'location/room/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/#' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/+/zanzito' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/#' + topic = 'somewhere/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 44c0e0c6295..2239e13e220 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -433,7 +433,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_gps_entry_exit(self): """Test the entry event.""" - # Entering the owntrack circular region named "inner" + # Entering the owntracks circular region named "inner" self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) # Enter uses the zone's gps co-ords @@ -447,7 +447,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): # note that LOCATION_MESSAGE is actually pretty far # from INNER_ZONE and has good accuracy. I haven't # received a transition message though so I'm still - # asssociated with the inner zone regardless of GPS. + # associated with the inner zone regardless of GPS. self.assert_location_latitude(INNER_ZONE['latitude']) self.assert_location_accuracy(INNER_ZONE['radius']) self.assert_location_state('inner') @@ -624,7 +624,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_entry_zone_loading_dash(self): """Test the event for zone landing.""" # Make sure the leading - is ignored - # Ownracks uses this to switch on hold + # Owntracks uses this to switch on hold message = build_message( {'desc': "-inner"}, REGION_GPS_ENTER_MESSAGE) @@ -673,10 +673,10 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_source_type_entry_exit(self): """Test the entry and exit events of source type.""" - # Entering the owntrack circular region named "inner" + # Entering the owntracks circular region named "inner" self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) - # source_type should be gps when enterings using gps. + # source_type should be gps when entering using gps. self.assert_location_source_type('gps') # owntracks shouldn't send beacon events with acc = 0 @@ -715,7 +715,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): # note that LOCATION_MESSAGE is actually pretty far # from INNER_ZONE and has good accuracy. I haven't # received a transition message though so I'm still - # asssociated with the inner zone regardless of GPS. + # associated with the inner zone regardless of GPS. self.assert_location_latitude(INNER_ZONE['latitude']) self.assert_location_accuracy(INNER_ZONE['radius']) self.assert_location_state('inner') @@ -865,7 +865,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_beacon_entry_zone_loading_dash(self): """Test the event for beacon zone landing.""" # Make sure the leading - is ignored - # Ownracks uses this to switch on hold + # Owntracks uses this to switch on hold message = build_message( {'desc': "-inner"}, diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index b378118141a..8bc3a60146c 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -139,12 +139,12 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): devices = scanner._get_update() # pylint: disable=protected-access self.assertTrue(devices is None) - def test_good_reponse_parses(self): + def test_good_response_parses(self): """Test that the response form the AP parses to JSON correctly.""" response = _response_to_json(load_fixture('unifi_direct.txt')) self.assertTrue(response != {}) - def test_bad_reponse_returns_none(self): + def test_bad_response_returns_none(self): """Test that a bad response form the AP parses to JSON correctly.""" self.assertTrue(_response_to_json("{(}") == {}) diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 94a4566a17b..19f25b514db 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -1,265 +1,265 @@ -"""The tests for the Xiaomi router device tracker platform.""" -import logging -import unittest -from unittest import mock -from unittest.mock import patch - -import requests - -from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi -from homeassistant.components.device_tracker.xiaomi import get_scanner -from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, - CONF_PLATFORM) -from tests.common import get_test_home_assistant - -_LOGGER = logging.getLogger(__name__) - -INVALID_USERNAME = 'bob' -TOKEN_TIMEOUT_USERNAME = 'tok' -URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login' -URL_LIST_END = 'api/misystem/devicelist' - -FIRST_CALL = True - - -def mocked_requests(*args, **kwargs): - """Mock requests.get invocations.""" - class MockResponse: - """Class to represent a mocked response.""" - - def __init__(self, json_data, status_code): - """Initialize the mock response class.""" - self.json_data = json_data - self.status_code = status_code - - def json(self): - """Return the json of the response.""" - return self.json_data - - @property - def content(self): - """Return the content of the response.""" - return self.json() - - def raise_for_status(self): - """Raise an HTTPError if status is not 200.""" - if self.status_code != 200: - raise requests.HTTPError(self.status_code) - - data = kwargs.get('data') - global FIRST_CALL - - if data and data.get('username', None) == INVALID_USERNAME: - # deliver an invalid token - return MockResponse({ - "code": "401", - "msg": "Invalid token" - }, 200) - elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: - # deliver an expired token - return MockResponse({ - "url": "/cgi-bin/luci/;stok=ef5860/web/home", - "token": "timedOut", - "code": "0" - }, 200) - elif str(args[0]).startswith(URL_AUTHORIZE): - # deliver an authorized token - return MockResponse({ - "url": "/cgi-bin/luci/;stok=ef5860/web/home", - "token": "ef5860", - "code": "0" - }, 200) - elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ - and FIRST_CALL is True: - FIRST_CALL = False - # deliver an error when called with expired token - return MockResponse({ - "code": "401", - "msg": "Invalid token" - }, 200) - elif str(args[0]).endswith(URL_LIST_END): - # deliver the device list - return MockResponse({ - "mac": "1C:98:EC:0E:D5:A4", - "list": [ - { - "mac": "23:83:BF:F6:38:A0", - "oname": "12255ff", - "isap": 0, - "parent": "", - "authority": { - "wan": 1, - "pridisk": 0, - "admin": 1, - "lan": 0 - }, - "push": 0, - "online": 1, - "name": "Device1", - "times": 0, - "ip": [ - { - "downspeed": "0", - "online": "496957", - "active": 1, - "upspeed": "0", - "ip": "192.168.0.25" - } - ], - "statistics": { - "downspeed": "0", - "online": "496957", - "upspeed": "0" - }, - "icon": "", - "type": 1 - }, - { - "mac": "1D:98:EC:5E:D5:A6", - "oname": "CdddFG58", - "isap": 0, - "parent": "", - "authority": { - "wan": 1, - "pridisk": 0, - "admin": 1, - "lan": 0 - }, - "push": 0, - "online": 1, - "name": "Device2", - "times": 0, - "ip": [ - { - "downspeed": "0", - "online": "347325", - "active": 1, - "upspeed": "0", - "ip": "192.168.0.3" - } - ], - "statistics": { - "downspeed": "0", - "online": "347325", - "upspeed": "0" - }, - "icon": "", - "type": 0 - }, - ], - "code": 0 - }, 200) - else: - _LOGGER.debug('UNKNOWN ROUTE') - - -class TestXiaomiDeviceScanner(unittest.TestCase): - """Xiaomi device scanner test class.""" - - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', - return_value=mock.MagicMock()) - def test_config(self, xiaomi_mock): - """Testing minimal configuration.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_PASSWORD: 'passwordTest' - }) - } - xiaomi.get_scanner(self.hass, config) - self.assertEqual(xiaomi_mock.call_count, 1) - self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) - call_arg = xiaomi_mock.call_args[0][0] - self.assertEqual(call_arg['username'], 'admin') - self.assertEqual(call_arg['password'], 'passwordTest') - self.assertEqual(call_arg['host'], '192.168.0.1') - self.assertEqual(call_arg['platform'], 'device_tracker') - - @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', - return_value=mock.MagicMock()) - def test_config_full(self, xiaomi_mock): - """Testing full configuration.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: 'alternativeAdminName', - CONF_PASSWORD: 'passwordTest' - }) - } - xiaomi.get_scanner(self.hass, config) - self.assertEqual(xiaomi_mock.call_count, 1) - self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) - call_arg = xiaomi_mock.call_args[0][0] - self.assertEqual(call_arg['username'], 'alternativeAdminName') - self.assertEqual(call_arg['password'], 'passwordTest') - self.assertEqual(call_arg['host'], '192.168.0.1') - self.assertEqual(call_arg['platform'], 'device_tracker') - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_invalid_credential(self, mock_get, mock_post): - """"Testing invalid credential handling.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: INVALID_USERNAME, - CONF_PASSWORD: 'passwordTest' - }) - } - self.assertIsNone(get_scanner(self.hass, config)) - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_valid_credential(self, mock_get, mock_post): - """"Testing valid refresh.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: 'admin', - CONF_PASSWORD: 'passwordTest' - }) - } - scanner = get_scanner(self.hass, config) - self.assertIsNotNone(scanner) - self.assertEqual(2, len(scanner.scan_devices())) - self.assertEqual("Device1", - scanner.get_device_name("23:83:BF:F6:38:A0")) - self.assertEqual("Device2", - scanner.get_device_name("1D:98:EC:5E:D5:A6")) - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_token_timed_out(self, mock_get, mock_post): - """"Testing refresh with a timed out token. - - New token is requested and list is downloaded a second time. - """ - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: TOKEN_TIMEOUT_USERNAME, - CONF_PASSWORD: 'passwordTest' - }) - } - scanner = get_scanner(self.hass, config) - self.assertIsNotNone(scanner) - self.assertEqual(2, len(scanner.scan_devices())) - self.assertEqual("Device1", - scanner.get_device_name("23:83:BF:F6:38:A0")) - self.assertEqual("Device2", - scanner.get_device_name("1D:98:EC:5E:D5:A6")) +"""The tests for the Xiaomi router device tracker platform.""" +import logging +import unittest +from unittest import mock +from unittest.mock import patch + +import requests + +from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi +from homeassistant.components.device_tracker.xiaomi import get_scanner +from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM) +from tests.common import get_test_home_assistant + +_LOGGER = logging.getLogger(__name__) + +INVALID_USERNAME = 'bob' +TOKEN_TIMEOUT_USERNAME = 'tok' +URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login' +URL_LIST_END = 'api/misystem/devicelist' + +FIRST_CALL = True + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + data = kwargs.get('data') + global FIRST_CALL + + if data and data.get('username', None) == INVALID_USERNAME: + # deliver an invalid token + return MockResponse({ + "code": "401", + "msg": "Invalid token" + }, 200) + elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: + # deliver an expired token + return MockResponse({ + "url": "/cgi-bin/luci/;stok=ef5860/web/home", + "token": "timedOut", + "code": "0" + }, 200) + elif str(args[0]).startswith(URL_AUTHORIZE): + # deliver an authorized token + return MockResponse({ + "url": "/cgi-bin/luci/;stok=ef5860/web/home", + "token": "ef5860", + "code": "0" + }, 200) + elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ + and FIRST_CALL is True: + FIRST_CALL = False + # deliver an error when called with expired token + return MockResponse({ + "code": "401", + "msg": "Invalid token" + }, 200) + elif str(args[0]).endswith(URL_LIST_END): + # deliver the device list + return MockResponse({ + "mac": "1C:98:EC:0E:D5:A4", + "list": [ + { + "mac": "23:83:BF:F6:38:A0", + "oname": "12255ff", + "isap": 0, + "parent": "", + "authority": { + "wan": 1, + "pridisk": 0, + "admin": 1, + "lan": 0 + }, + "push": 0, + "online": 1, + "name": "Device1", + "times": 0, + "ip": [ + { + "downspeed": "0", + "online": "496957", + "active": 1, + "upspeed": "0", + "ip": "192.168.0.25" + } + ], + "statistics": { + "downspeed": "0", + "online": "496957", + "upspeed": "0" + }, + "icon": "", + "type": 1 + }, + { + "mac": "1D:98:EC:5E:D5:A6", + "oname": "CdddFG58", + "isap": 0, + "parent": "", + "authority": { + "wan": 1, + "pridisk": 0, + "admin": 1, + "lan": 0 + }, + "push": 0, + "online": 1, + "name": "Device2", + "times": 0, + "ip": [ + { + "downspeed": "0", + "online": "347325", + "active": 1, + "upspeed": "0", + "ip": "192.168.0.3" + } + ], + "statistics": { + "downspeed": "0", + "online": "347325", + "upspeed": "0" + }, + "icon": "", + "type": 0 + }, + ], + "code": 0 + }, 200) + else: + _LOGGER.debug('UNKNOWN ROUTE') + + +class TestXiaomiDeviceScanner(unittest.TestCase): + """Xiaomi device scanner test class.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch( + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', + return_value=mock.MagicMock()) + def test_config(self, xiaomi_mock): + """Testing minimal configuration.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_PASSWORD: 'passwordTest' + }) + } + xiaomi.get_scanner(self.hass, config) + self.assertEqual(xiaomi_mock.call_count, 1) + self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) + call_arg = xiaomi_mock.call_args[0][0] + self.assertEqual(call_arg['username'], 'admin') + self.assertEqual(call_arg['password'], 'passwordTest') + self.assertEqual(call_arg['host'], '192.168.0.1') + self.assertEqual(call_arg['platform'], 'device_tracker') + + @mock.patch( + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', + return_value=mock.MagicMock()) + def test_config_full(self, xiaomi_mock): + """Testing full configuration.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: 'alternativeAdminName', + CONF_PASSWORD: 'passwordTest' + }) + } + xiaomi.get_scanner(self.hass, config) + self.assertEqual(xiaomi_mock.call_count, 1) + self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) + call_arg = xiaomi_mock.call_args[0][0] + self.assertEqual(call_arg['username'], 'alternativeAdminName') + self.assertEqual(call_arg['password'], 'passwordTest') + self.assertEqual(call_arg['host'], '192.168.0.1') + self.assertEqual(call_arg['platform'], 'device_tracker') + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_invalid_credential(self, mock_get, mock_post): + """"Testing invalid credential handling.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: INVALID_USERNAME, + CONF_PASSWORD: 'passwordTest' + }) + } + self.assertIsNone(get_scanner(self.hass, config)) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_valid_credential(self, mock_get, mock_post): + """"Testing valid refresh.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: 'admin', + CONF_PASSWORD: 'passwordTest' + }) + } + scanner = get_scanner(self.hass, config) + self.assertIsNotNone(scanner) + self.assertEqual(2, len(scanner.scan_devices())) + self.assertEqual("Device1", + scanner.get_device_name("23:83:BF:F6:38:A0")) + self.assertEqual("Device2", + scanner.get_device_name("1D:98:EC:5E:D5:A6")) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_token_timed_out(self, mock_get, mock_post): + """"Testing refresh with a timed out token. + + New token is requested and list is downloaded a second time. + """ + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: TOKEN_TIMEOUT_USERNAME, + CONF_PASSWORD: 'passwordTest' + }) + } + scanner = get_scanner(self.hass, config) + self.assertIsNotNone(scanner) + self.assertEqual(2, len(scanner.scan_devices())) + self.assertEqual("Device1", + scanner.get_device_name("23:83:BF:F6:38:A0")) + self.assertEqual("Device2", + scanner.get_device_name("1D:98:EC:5E:D5:A6")) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 383b4f7165d..cba3c835763 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -121,7 +121,14 @@ def hass_hue(loop, hass): def hue_client(loop, hass_hue, test_client): """Create web client for emulated hue api.""" web_app = hass_hue.http.app - config = Config(None, {'type': 'alexa'}) + config = Config(None, { + emulated_hue.CONF_TYPE: emulated_hue.TYPE_ALEXA, + emulated_hue.CONF_ENTITIES: { + 'light.bed_light': { + emulated_hue.CONF_ENTITY_HIDDEN: True + } + } + }) HueUsernameView().register(web_app.router) HueAllLightsStateView(config).register(web_app.router) @@ -145,7 +152,7 @@ def test_discover_lights(hue_client): # Make sure the lights we added to the config are there assert 'light.ceiling_lights' in devices - assert 'light.bed_light' in devices + assert 'light.bed_light' not in devices assert 'script.set_kitchen_light' in devices assert 'light.kitchen_lights' not in devices assert 'media_player.living_room' in devices @@ -186,19 +193,23 @@ def test_get_light_state(hass_hue, hue_client): assert result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI] == \ 127 - # Turn bedroom light off + # Turn office light off yield from hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, { - const.ATTR_ENTITY_ID: 'light.bed_light' + const.ATTR_ENTITY_ID: 'light.ceiling_lights' }, blocking=True) - bedroom_json = yield from perform_get_light_state( - hue_client, 'light.bed_light', 200) + office_json = yield from perform_get_light_state( + hue_client, 'light.ceiling_lights', 200) - assert bedroom_json['state'][HUE_API_STATE_ON] is False - assert bedroom_json['state'][HUE_API_STATE_BRI] == 0 + assert office_json['state'][HUE_API_STATE_ON] is False + assert office_json['state'][HUE_API_STATE_BRI] == 0 + + # Make sure bedroom light isn't accessible + yield from perform_get_light_state( + hue_client, 'light.bed_light', 404) # Make sure kitchen light isn't accessible yield from perform_get_light_state( @@ -207,35 +218,41 @@ def test_get_light_state(hass_hue, hue_client): @asyncio.coroutine def test_put_light_state(hass_hue, hue_client): - """Test the seeting of light states.""" + """Test the setting of light states.""" yield from perform_put_test_on_ceiling_lights(hass_hue, hue_client) # Turn the bedroom light on first yield from hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, - {const.ATTR_ENTITY_ID: 'light.bed_light', + {const.ATTR_ENTITY_ID: 'light.ceiling_lights', light.ATTR_BRIGHTNESS: 153}, blocking=True) - bed_light = hass_hue.states.get('light.bed_light') - assert bed_light.state == STATE_ON - assert bed_light.attributes[light.ATTR_BRIGHTNESS] == 153 + ceiling_lights = hass_hue.states.get('light.ceiling_lights') + assert ceiling_lights.state == STATE_ON + assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153 # Go through the API to turn it off - bedroom_result = yield from perform_put_light_state( + ceiling_result = yield from perform_put_light_state( hass_hue, hue_client, - 'light.bed_light', False) + 'light.ceiling_lights', False) - bedroom_result_json = yield from bedroom_result.json() + ceiling_result_json = yield from ceiling_result.json() - assert bedroom_result.status == 200 - assert 'application/json' in bedroom_result.headers['content-type'] + assert ceiling_result.status == 200 + assert 'application/json' in ceiling_result.headers['content-type'] - assert len(bedroom_result_json) == 1 + assert len(ceiling_result_json) == 1 # Check to make sure the state changed - bed_light = hass_hue.states.get('light.bed_light') - assert bed_light.state == STATE_OFF + ceiling_lights = hass_hue.states.get('light.ceiling_lights') + assert ceiling_lights.state == STATE_OFF + + # Make sure we can't change the bedroom light state + bedroom_result = yield from perform_put_light_state( + hass_hue, hue_client, + 'light.bed_light', True) + assert bedroom_result.status == 404 # Make sure we can't change the kitchen light state kitchen_result = yield from perform_put_light_state( @@ -435,7 +452,7 @@ def perform_put_test_on_ceiling_lights(hass_hue, hue_client, @asyncio.coroutine def perform_get_light_state(client, entity_id, expected_status): - """Test the gettting of a light state.""" + """Test the getting of a light state.""" result = yield from client.get('/api/username/lights/{}'.format(entity_id)) assert result.status == expected_status diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 25bcbc1dd55..06613f1336a 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,128 +1,128 @@ -"""Test the Emulated Hue component.""" -import json - -from unittest.mock import patch, Mock, mock_open - -from homeassistant.components.emulated_hue import Config, _LOGGER - - -def test_config_google_home_entity_id_to_number(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 - - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' - - -def test_config_google_home_entity_id_to_number_altered(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 - - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' - - -def test_config_google_home_entity_id_to_number_empty(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data='') - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 - - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' - - -def test_config_alexa_entity_id_to_number(): - """Test config adheres to the type.""" - conf = Config(None, { - 'type': 'alexa' - }) - - number = conf.entity_id_to_number('light.test') - assert number == 'light.test' - - number = conf.entity_id_to_number('light.test') - assert number == 'light.test' - - number = conf.entity_id_to_number('light.test2') - assert number == 'light.test2' - - entity_id = conf.number_to_entity_id('light.test') - assert entity_id == 'light.test' - - -def test_warning_config_google_home_listen_port(): - """Test we warn when non-default port is used for Google Home.""" - with patch.object(_LOGGER, 'warning') as mock_warn: - Config(None, { - 'type': 'google_home', - 'host_ip': '123.123.123.123', - 'listen_port': 8300 - }) - - assert mock_warn.called - assert mock_warn.mock_calls[0][1][0] == \ - "When targeting Google Home, listening port has to be port 80" +"""Test the Emulated Hue component.""" +import json + +from unittest.mock import patch, Mock, mock_open + +from homeassistant.components.emulated_hue import Config, _LOGGER + + +def test_config_google_home_entity_id_to_number(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' + + +def test_config_google_home_entity_id_to_number_altered(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' + + +def test_config_google_home_entity_id_to_number_empty(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data='') + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 + + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' + + +def test_config_alexa_entity_id_to_number(): + """Test config adheres to the type.""" + conf = Config(None, { + 'type': 'alexa' + }) + + number = conf.entity_id_to_number('light.test') + assert number == 'light.test' + + number = conf.entity_id_to_number('light.test') + assert number == 'light.test' + + number = conf.entity_id_to_number('light.test2') + assert number == 'light.test2' + + entity_id = conf.number_to_entity_id('light.test') + assert entity_id == 'light.test' + + +def test_warning_config_google_home_listen_port(): + """Test we warn when non-default port is used for Google Home.""" + with patch.object(_LOGGER, 'warning') as mock_warn: + Config(None, { + 'type': 'google_home', + 'host_ip': '123.123.123.123', + 'listen_port': 8300 + }) + + assert mock_warn.called + assert mock_warn.mock_calls[0][1][0] == \ + "When targeting Google Home, listening port has to be port 80" diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 1cd895954de..b3032954431 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -87,10 +87,9 @@ class TestEmulatedHue(unittest.TestCase): self.assertTrue('text/xml' in result.headers['content-type']) # Make sure the XML is parsable - # pylint: disable=bare-except try: ET.fromstring(result.text) - except: + except: # noqa: E722 # pylint: disable=bare-except self.fail('description.xml is not valid XML!') def test_create_username(self): diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0d87b491229..43c36d1ca2a 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -175,6 +175,8 @@ def test_query_request(hass_fixture, assistant_client): 'id': "light.bed_light", }, { 'id': "light.kitchen_lights", + }, { + 'id': 'media_player.lounge_room', }] } }] @@ -187,12 +189,14 @@ def test_query_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid devices = body['payload']['devices'] - assert len(devices) == 3 + assert len(devices) == 4 assert devices['light.bed_light']['on'] is False assert devices['light.ceiling_lights']['on'] is True assert devices['light.ceiling_lights']['brightness'] == 70 assert devices['light.kitchen_lights']['color']['spectrumRGB'] == 16727919 assert devices['light.kitchen_lights']['color']['temperature'] == 4166 + assert devices['media_player.lounge_room']['on'] is True + assert devices['media_player.lounge_room']['brightness'] == 100 @asyncio.coroutine @@ -225,26 +229,36 @@ def test_query_climate_request(hass_fixture, assistant_client): devices = body['payload']['devices'] assert devices == { 'climate.heatpump': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': 20.0, 'thermostatTemperatureAmbient': 25.0, 'thermostatMode': 'heat', }, 'climate.ecobee': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, 'thermostatMode': 'heat', 'thermostatTemperatureSetpointLow': 21 }, 'climate.hvac': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': 21, 'thermostatTemperatureAmbient': 22, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, }, 'sensor.outside_temperature': { + 'on': True, + 'online': True, 'thermostatTemperatureAmbient': 15.6 }, 'sensor.outside_humidity': { + 'on': True, + 'online': True, 'thermostatHumidityAmbient': 54.0 } } @@ -280,23 +294,31 @@ def test_query_climate_request_f(hass_fixture, assistant_client): devices = body['payload']['devices'] assert devices == { 'climate.heatpump': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': -6.7, 'thermostatTemperatureAmbient': -3.9, 'thermostatMode': 'heat', }, 'climate.ecobee': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, 'thermostatMode': 'heat', 'thermostatTemperatureSetpointLow': -6.1, }, 'climate.hvac': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': -6.1, 'thermostatTemperatureAmbient': -5.6, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, }, 'sensor.outside_temperature': { + 'on': True, + 'online': True, 'thermostatTemperatureAmbient': -9.1 } } @@ -304,7 +326,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): @asyncio.coroutine def test_execute_request(hass_fixture, assistant_client): - """Test a execute request.""" + """Test an execute request.""" reqid = '5711642932632160985' data = { 'requestId': @@ -317,6 +339,8 @@ def test_execute_request(hass_fixture, assistant_client): "id": "light.ceiling_lights", }, { "id": "switch.decorative_lights", + }, { + "id": "media_player.lounge_room", }], "execution": [{ "command": "action.devices.commands.OnOff", @@ -324,6 +348,17 @@ def test_execute_request(hass_fixture, assistant_client): "on": False } }] + }, { + "devices": [{ + "id": "media_player.walkman", + }], + "execution": [{ + "command": + "action.devices.commands.BrightnessAbsolute", + "params": { + "brightness": 70 + } + }] }, { "devices": [{ "id": "light.kitchen_lights", @@ -380,7 +415,7 @@ def test_execute_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid commands = body['payload']['commands'] - assert len(commands) == 6 + assert len(commands) == 8 ceiling = hass_fixture.states.get('light.ceiling_lights') assert ceiling.state == 'off' @@ -394,3 +429,10 @@ def test_execute_request(hass_fixture, assistant_client): assert bed.attributes.get(light.ATTR_RGB_COLOR) == (0, 255, 0) assert hass_fixture.states.get('switch.decorative_lights').state == 'off' + + walkman = hass_fixture.states.get('media_player.walkman') + assert walkman.state == 'playing' + assert walkman.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) == 0.7 + + lounge = hass_fixture.states.get('media_player.lounge_room') + assert lounge.state == 'off' diff --git a/tests/components/image_processing/__init__.py b/tests/components/image_processing/__init__.py index 6e79d49c251..63aee1dfbaf 100644 --- a/tests/components/image_processing/__init__.py +++ b/tests/components/image_processing/__init__.py @@ -1 +1 @@ -"""Test 'image_processing' component plaforms.""" +"""Test 'image_processing' component platforms.""" diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 0594c436abd..628c5405eaa 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -82,7 +82,7 @@ class TestImageProcessing(object): @patch('homeassistant.components.camera.demo.DemoCamera.camera_image', autospec=True, return_value=b'Test') def test_get_image_from_camera(self, mock_camera): - """Grab a image from camera entity.""" + """Grab an image from camera entity.""" self.hass.start() ip.scan(self.hass, entity_id='image_processing.test') @@ -275,7 +275,7 @@ class TestImageProcessingFace(object): @patch('homeassistant.components.image_processing.demo.' 'DemoImageProcessingFace.confidence', new_callable=PropertyMock(return_value=None)) - def test_face_event_call_no_confidence(self, mock_confi, aioclient_mock): + def test_face_event_call_no_confidence(self, mock_config, aioclient_mock): """Setup and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index e35ac8185d0..e840bce54f7 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -1,4 +1,4 @@ -"""The tests for the openalpr clooud platform.""" +"""The tests for the openalpr cloud platform.""" import asyncio from unittest.mock import patch, PropertyMock @@ -13,7 +13,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture) -class TestOpenAlprCloudlSetup(object): +class TestOpenAlprCloudSetup(object): """Test class for image processing.""" def setup_method(self): @@ -149,8 +149,7 @@ class TestOpenAlprCloud(object): 'secret_key': "sk_abcxyz123456", 'tasks': "plate", 'return_image': 0, - 'country': 'eu', - 'image_bytes': "aW1hZ2U=" + 'country': 'eu' } def teardown_method(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 611f1240d45..5c28ea9988f 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -68,7 +68,7 @@ class TestSetup(unittest.TestCase): """Return a dict suitable for mocking api.get('lights').""" mock_bridge_lights = lights - for light_id, info in mock_bridge_lights.items(): + for info in mock_bridge_lights.values(): if 'state' not in info: info['state'] = {'on': False} @@ -272,38 +272,20 @@ class TestSetup(unittest.TestCase): hue_light.unthrottled_update_lights( self.hass, mock_bridge_two, self.mock_add_devices) - self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2]) - self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3]) + self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2]) + self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3]) - self.assertEquals(len(self.mock_add_devices.mock_calls), 2) + self.assertEqual(len(self.mock_add_devices.mock_calls), 2) # first call name, args, kwargs = self.mock_add_devices.mock_calls[0] - self.assertEquals(len(args), 1) - self.assertEquals(len(kwargs), 0) - - # one argument, a list of lights in bridge one; each of them is an - # object of type HueLight so we can't straight up compare them - lights = args[0] - self.assertEquals( - lights[0].unique_id, - '{}.b1l1.Light.1'.format(hue_light.HueLight)) - self.assertEquals( - lights[1].unique_id, - '{}.b1l2.Light.2'.format(hue_light.HueLight)) + 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.assertEquals(len(args), 1) - self.assertEquals(len(kwargs), 0) - - lights = args[0] - self.assertEquals( - lights[0].unique_id, - '{}.b2l1.Light.1'.format(hue_light.HueLight)) - self.assertEquals( - lights[1].unique_id, - '{}.b2l3.Light.3'.format(hue_light.HueLight)) + 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.""" @@ -313,8 +295,8 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lights, {}) + 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.""" @@ -325,9 +307,9 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) + self.assertEqual([], ret) mock_dispatcher_send.assert_not_called() - self.assertEquals(self.mock_bridge.lights, {}) + self.assertEqual(self.mock_bridge.lights, {}) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_some_lights(self, mock_hue_light): @@ -341,7 +323,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) + self.assertEqual(len(ret), 2) mock_hue_light.assert_has_calls([ call( 1, {'state': 'on'}, self.mock_bridge, mock.ANY, @@ -353,7 +335,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge.allow_in_emulated_hue), ]) mock_dispatcher_send.assert_not_called() - self.assertEquals(len(self.mock_bridge.lights), 2) + self.assertEqual(len(self.mock_bridge.lights), 2) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_new_light(self, mock_hue_light): @@ -373,7 +355,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) + self.assertEqual(len(ret), 1) mock_hue_light.assert_has_calls([ call( 2, {'state': 'off'}, self.mock_bridge, mock.ANY, @@ -382,7 +364,7 @@ class TestSetup(unittest.TestCase): ]) mock_dispatcher_send.assert_called_once_with( 'hue_light_callback_bridge-id_1') - self.assertEquals(len(self.mock_bridge.lights), 2) + 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.""" @@ -392,8 +374,8 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lightgroups, {}) + 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.""" @@ -405,9 +387,9 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) + self.assertEqual([], ret) mock_dispatcher_send.assert_not_called() - self.assertEquals(self.mock_bridge.lightgroups, {}) + self.assertEqual(self.mock_bridge.lightgroups, {}) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_some_groups(self, mock_hue_light): @@ -421,7 +403,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) + self.assertEqual(len(ret), 2) mock_hue_light.assert_has_calls([ call( 1, {'state': 'on'}, self.mock_bridge, mock.ANY, @@ -433,7 +415,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge.allow_in_emulated_hue, True), ]) mock_dispatcher_send.assert_not_called() - self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.assertEqual(len(self.mock_bridge.lightgroups), 2) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_new_group(self, mock_hue_light): @@ -453,7 +435,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) + self.assertEqual(len(ret), 1) mock_hue_light.assert_has_calls([ call( 2, {'state': 'off'}, self.mock_bridge, mock.ANY, @@ -462,7 +444,7 @@ class TestSetup(unittest.TestCase): ]) mock_dispatcher_send.assert_called_once_with( 'hue_light_callback_bridge-id_1') - self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.assertEqual(len(self.mock_bridge.lightgroups), 2) class TestHueLight(unittest.TestCase): @@ -506,60 +488,16 @@ class TestHueLight(unittest.TestCase): def test_unique_id_for_light(self): """Test the unique_id method with lights.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEquals( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}) - self.assertEquals( - class_name+'.Unnamed Device.Light.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}) - self.assertEquals( - class_name+'.my-name.Light.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}) - self.assertEquals( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight(info={'name': 'a name', 'type': 'my-type'}) - self.assertEquals( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) def test_unique_id_for_group(self): """Test the unique_id method with groups.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEquals( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}, is_group=True) - self.assertEquals( - class_name+'.Unnamed Device.Group.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}, is_group=True) - self.assertEquals( - class_name+'.my-name.Group.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}, is_group=True) - self.assertEquals( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight( - info={'name': 'a name', 'type': 'my-type'}, - is_group=True) - self.assertEquals( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py index 001c419066f..dd4b4b4a56e 100644 --- a/tests/components/light/test_litejet.py +++ b/tests/components/light/test_litejet.py @@ -156,7 +156,7 @@ class TestLiteJetLight(unittest.TestCase): # (Requesting the level is not strictly needed with a deactivated # event but the implementation happens to do it. This could be - # changed to a assert_not_called in the future.) + # changed to an assert_not_called in the future.) self.mock_lj.get_load_level.assert_called_with( ENTITY_OTHER_LIGHT_NUMBER) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 5cb0d0cdc1b..a06f8e7d093 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -1,579 +1,579 @@ -"""The tests for the MQTT JSON light platform. - -Configuration with RGB, brightness, color temp, effect, white value and XY: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - xy: true - -Configuration with RGB, brightness, color temp, effect, white value: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - -Configuration with RGB, brightness, color temp and effect: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - -Configuration with RGB, brightness and color temp: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - color_temp: true - -Configuration with RGB, brightness: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - -Config without RGB: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - -Config without RGB and brightness: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - -Config with brightness and scale: - -light: - platform: mqtt_json - name: test - state_topic: "mqtt_json_light_1" - command_topic: "mqtt_json_light_1/set" - brightness: true - brightness_scale: 99 -""" - -import json -import unittest - -from homeassistant.setup import setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, - ATTR_SUPPORTED_FEATURES) -import homeassistant.components.light as light -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) - - -class TestLightMQTTJSON(unittest.TestCase): - """Test the MQTT JSON light.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_fail_setup_if_no_command_topic(self): \ - # pylint: disable=invalid-name - """Test if setup fails with no command topic.""" - with assert_setup_component(0, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - } - }) - self.assertIsNone(self.hass.states.get('light.test')) - - def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \ - # pylint: disable=invalid-name - """Test for no RGB, brightness, color temp, effect, white val or XY.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, 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')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - - def test_controlling_state_via_topic(self): \ - # pylint: disable=invalid-name - """Test the controlling of the state via topic.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'xy': True, - 'qos': '0' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(255, 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')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # 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},' - '"brightness":255,' - '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}') - self.hass.block_till_done() - - 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, 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')) - - # Turn the light off - fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"brightness":100}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(100, - light_state.attributes['brightness']) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":125,"g":125,"b":125}}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], - light_state.attributes.get('rgb_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"x":0.135,"y":0.135}}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([0.135, 0.135], - light_state.attributes.get('xy_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color_temp":155}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual(155, light_state.attributes.get('color_temp')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"effect":"colorloop"}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual('colorloop', light_state.attributes.get('effect')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"white_value":155}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual(155, light_state.attributes.get('white_value')) - - def test_sending_mqtt_commands_and_optimistic(self): \ - # pylint: disable=invalid-name - """Test the sending of command in optimistic mode.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'qos': 2 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - - light.turn_on(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', '{"state": "ON"}', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - light.turn_off(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', '{"state": "OFF"}', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - light.turn_on(self.hass, 'light.test', - brightness=50, color_temp=155, effect='colorloop', - white_value=170) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(50, message_json["brightness"]) - self.assertEqual(155, message_json["color_temp"]) - self.assertEqual('colorloop', message_json["effect"]) - self.assertEqual(170, message_json["white_value"]) - self.assertEqual("ON", message_json["state"]) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(50, state.attributes['brightness']) - self.assertEqual(155, state.attributes['color_temp']) - self.assertEqual('colorloop', state.attributes['effect']) - self.assertEqual(170, state.attributes['white_value']) - - def test_flash_short_and_long(self): \ - # pylint: disable=invalid-name - """Test for flash length being sent when included.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'flash_time_short': 5, - 'flash_time_long': 15, - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - - light.turn_on(self.hass, 'light.test', flash="short") - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(5, message_json["flash"]) - self.assertEqual("ON", message_json["state"]) - - light.turn_on(self.hass, 'light.test', flash="long") - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(15, message_json["flash"]) - self.assertEqual("ON", message_json["state"]) - - def test_transition(self): - """Test for transition time being sent when included.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - - light.turn_on(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(10, message_json["transition"]) - self.assertEqual("ON", message_json["state"]) - - # Transition back off - light.turn_off(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(10, message_json["transition"]) - self.assertEqual("OFF", message_json["state"]) - - def test_brightness_scale(self): - """Test for brightness scaling.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_bright_scale', - 'command_topic': 'test_light_bright_scale/set', - 'brightness': True, - 'brightness_scale': 99 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('brightness')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # Turn on the light - fire_mqtt_message(self.hass, 'test_light_bright_scale', - '{"state":"ON"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - # Turn on the light with brightness - fire_mqtt_message(self.hass, 'test_light_bright_scale', - '{"state":"ON",' - '"brightness": 99}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - def test_invalid_color_brightness_and_white_values(self): \ - # pylint: disable=invalid-name - """Test that invalid color/brightness/white values are ignored.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'rgb': True, - 'white_value': True, - 'qos': '0' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # Turn on the light - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255},' - '"brightness": 255,' - '"white_value": 255}') - self.hass.block_till_done() - - 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, state.attributes.get('brightness')) - self.assertEqual(255, state.attributes.get('white_value')) - - # Bad color values - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":"bad","g":"val","b":"test"}}') - self.hass.block_till_done() - - # 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')) - - # Bad brightness values - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"brightness": "badValue"}') - self.hass.block_till_done() - - # Brightness should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - # Bad white value - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"white_value": "badValue"}') - self.hass.block_till_done() - - # White value should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('white_value')) - - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'availability_topic': 'availability-topic' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) +"""The tests for the MQTT JSON light platform. + +Configuration with RGB, brightness, color temp, effect, white value and XY: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + white_value: true + xy: true + +Configuration with RGB, brightness, color temp, effect, white value: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + white_value: true + +Configuration with RGB, brightness, color temp and effect: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + +Configuration with RGB, brightness and color temp: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + color_temp: true + +Configuration with RGB, brightness: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + +Config without RGB: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + +Config without RGB and brightness: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + +Config with brightness and scale: + +light: + platform: mqtt_json + name: test + state_topic: "mqtt_json_light_1" + command_topic: "mqtt_json_light_1/set" + brightness: true + brightness_scale: 99 +""" + +import json +import unittest + +from homeassistant.setup import setup_component +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES) +import homeassistant.components.light as light +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, + assert_setup_component) + + +class TestLightMQTTJSON(unittest.TestCase): + """Test the MQTT JSON light.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_fail_setup_if_no_command_topic(self): \ + # pylint: disable=invalid-name + """Test if setup fails with no command topic.""" + with assert_setup_component(0, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + } + }) + self.assertIsNone(self.hass.states.get('light.test')) + + def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \ + # pylint: disable=invalid-name + """Test for no RGB, brightness, color temp, effect, white val or XY.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, 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')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + + def test_controlling_state_via_topic(self): \ + # pylint: disable=invalid-name + """Test the controlling of the state via topic.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'xy': True, + 'qos': '0' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(255, 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')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # 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},' + '"brightness":255,' + '"color_temp":155,' + '"effect":"colorloop",' + '"white_value":150}') + self.hass.block_till_done() + + 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, 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')) + + # Turn the light off + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"brightness":100}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(100, + light_state.attributes['brightness']) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":125,"g":125,"b":125}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([125, 125, 125], + light_state.attributes.get('rgb_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"x":0.135,"y":0.135}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([0.135, 0.135], + light_state.attributes.get('xy_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color_temp":155}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual(155, light_state.attributes.get('color_temp')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"effect":"colorloop"}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual('colorloop', light_state.attributes.get('effect')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"white_value":155}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual(155, light_state.attributes.get('white_value')) + + def test_sending_mqtt_commands_and_optimistic(self): \ + # pylint: disable=invalid-name + """Test the sending of command in optimistic mode.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'qos': 2 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) + + light.turn_on(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', '{"state": "ON"}', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + light.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', '{"state": "OFF"}', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + light.turn_on(self.hass, 'light.test', + brightness=50, color_temp=155, effect='colorloop', + white_value=170) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(50, message_json["brightness"]) + self.assertEqual(155, message_json["color_temp"]) + self.assertEqual('colorloop', message_json["effect"]) + self.assertEqual(170, message_json["white_value"]) + self.assertEqual("ON", message_json["state"]) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual(155, state.attributes['color_temp']) + self.assertEqual('colorloop', state.attributes['effect']) + self.assertEqual(170, state.attributes['white_value']) + + def test_flash_short_and_long(self): \ + # pylint: disable=invalid-name + """Test for flash length being sent when included.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'flash_time_short': 5, + 'flash_time_long': 15, + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + + light.turn_on(self.hass, 'light.test', flash="short") + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(5, message_json["flash"]) + self.assertEqual("ON", message_json["state"]) + + light.turn_on(self.hass, 'light.test', flash="long") + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(15, message_json["flash"]) + self.assertEqual("ON", message_json["state"]) + + def test_transition(self): + """Test for transition time being sent when included.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + + light.turn_on(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(10, message_json["transition"]) + self.assertEqual("ON", message_json["state"]) + + # Transition back off + light.turn_off(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(10, message_json["transition"]) + self.assertEqual("OFF", message_json["state"]) + + def test_brightness_scale(self): + """Test for brightness scaling.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_bright_scale', + 'command_topic': 'test_light_bright_scale/set', + 'brightness': True, + 'brightness_scale': 99 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('brightness')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # Turn on the light + fire_mqtt_message(self.hass, 'test_light_bright_scale', + '{"state":"ON"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + # Turn on the light with brightness + fire_mqtt_message(self.hass, 'test_light_bright_scale', + '{"state":"ON",' + '"brightness": 99}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + def test_invalid_color_brightness_and_white_values(self): \ + # pylint: disable=invalid-name + """Test that invalid color/brightness/white values are ignored.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'rgb': True, + 'white_value': True, + 'qos': '0' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # Turn on the light + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":255,"g":255,"b":255},' + '"brightness": 255,' + '"white_value": 255}') + self.hass.block_till_done() + + 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, state.attributes.get('brightness')) + self.assertEqual(255, state.attributes.get('white_value')) + + # Bad color values + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":"bad","g":"val","b":"test"}}') + self.hass.block_till_done() + + # 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')) + + # Bad brightness values + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"brightness": "badValue"}') + self.hass.block_till_done() + + # Brightness should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + # Bad white value + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"white_value": "badValue"}') + self.hass.block_till_done() + + # White value should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('white_value')) + + def test_default_availability_payload(self): + """Test availability by default payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index be1f119fc14..0df9d8136e1 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -1,524 +1,524 @@ -"""The tests for the MQTT Template light platform. - -Configuration example with all features: - -light: - platform: mqtt_template - name: mqtt_template_light_1 - state_topic: 'home/rgb1' - command_topic: 'home/rgb1/set' - command_on_template: > - on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} - command_off_template: 'off' - state_template: '{{ value.split(",")[0] }}' - brightness_template: '{{ value.split(",")[1] }}' - color_temp_template: '{{ value.split(",")[2] }}' - white_value_template: '{{ value.split(",")[3] }}' - red_template: '{{ value.split(",")[4].split("-")[0] }}' - green_template: '{{ value.split(",")[4].split("-")[1] }}' - blue_template: '{{ value.split(",")[4].split("-")[2] }}' - -If your light doesn't support brightness feature, omit `brightness_template`. - -If your light doesn't support color temp feature, omit `color_temp_template`. - -If your light doesn't support white value feature, omit `white_value_template`. - -If your light doesn't support RGB feature, omit `(red|green|blue)_template`. -""" -import unittest - -from homeassistant.setup import setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) -import homeassistant.components.light as light -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) - - -class TestLightMQTTTemplate(unittest.TestCase): - """Test the MQTT Template light.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_fails(self): \ - # pylint: disable=invalid-name - """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - } - }) - self.assertIsNone(self.hass.states.get('light.test')) - - def test_state_change_via_topic(self): \ - # pylint: disable=invalid-name - """Test state change via topic.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - fire_mqtt_message(self.hass, 'test_light_rgb', 'on') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - - def test_state_brightness_color_effect_temp_white_change_via_topic(self): \ - # pylint: disable=invalid-name - """Test state, bri, color, effect, color temp, white val change.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'effect_list': ['rainbow', 'colorloop'], - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }},' - '{{ effect|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}', - 'brightness_template': '{{ value.split(",")[1] }}', - 'color_temp_template': '{{ value.split(",")[2] }}', - 'white_value_template': '{{ value.split(",")[3] }}', - 'red_template': '{{ value.split(",")[4].' - 'split("-")[0] }}', - 'green_template': '{{ value.split(",")[4].' - 'split("-")[1] }}', - 'blue_template': '{{ value.split(",")[4].' - 'split("-")[2] }}', - 'effect_template': '{{ value.split(",")[5] }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,255,145,123,255-128-64,') - self.hass.block_till_done() - - 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, state.attributes.get('brightness')) - self.assertEqual(145, state.attributes.get('color_temp')) - self.assertEqual(123, state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('effect')) - - # turn the light off - fire_mqtt_message(self.hass, 'test_light_rgb', 'off') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # lower the brightness - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(100, light_state.attributes['brightness']) - - # change the color temp - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(195, light_state.attributes['color_temp']) - - # change the color - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) - - # change the white value - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(134, light_state.attributes['white_value']) - - # change the effect - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,,,,41-42-43,rainbow') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual('rainbow', light_state.attributes.get('effect')) - - def test_optimistic(self): \ - # pylint: disable=invalid-name - """Test optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'qos': 2 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light - light.turn_on(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', 'on,,,,--', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - # turn the light off - light.turn_off(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', 'off', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # turn on the light with brightness, color - light.turn_on(self.hass, 'light.test', brightness=50, - rgb_color=[75, 75, 75]) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,50,,,75-75-75', payload) - - # turn on the light with color temp and white val - light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) - self.hass.block_till_done() - - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,,200,139,--', payload) - - self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # 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(50, state.attributes['brightness']) - self.assertEqual(200, state.attributes['color_temp']) - self.assertEqual(139, state.attributes['white_value']) - - def test_flash(self): \ - # pylint: disable=invalid-name - """Test flash.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ flash }}', - 'command_off_template': 'off', - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # short flash - light.turn_on(self.hass, 'light.test', flash='short') - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,short', payload) - - # long flash - light.turn_on(self.hass, 'light.test', flash='long') - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,long', payload) - - def test_transition(self): - """Test for transition time being sent when included.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # transition on - light.turn_on(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,10', payload) - - # transition off - light.turn_off(self.hass, 'light.test', transition=4) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('off,4', payload) - - def test_invalid_values(self): \ - # pylint: disable=invalid-name - """Test that invalid values are ignored.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'effect_list': ['rainbow', 'colorloop'], - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }},' - '{{ effect|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}', - 'brightness_template': '{{ value.split(",")[1] }}', - 'color_temp_template': '{{ value.split(",")[2] }}', - 'white_value_template': '{{ value.split(",")[3] }}', - 'red_template': '{{ value.split(",")[4].' - 'split("-")[0] }}', - 'green_template': '{{ value.split(",")[4].' - 'split("-")[1] }}', - 'blue_template': '{{ value.split(",")[4].' - 'split("-")[2] }}', - 'effect_template': '{{ value.split(",")[5] }}', - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,255,215,222,255-255-255,rainbow') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - 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(222, state.attributes.get('white_value')) - self.assertEqual('rainbow', state.attributes.get('effect')) - - # bad state value - fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') - self.hass.block_till_done() - - # state should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - # bad brightness values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255') - self.hass.block_till_done() - - # brightness should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(255, state.attributes.get('brightness')) - - # bad color temp values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255') - self.hass.block_till_done() - - # color temp should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(215, state.attributes.get('color_temp')) - - # bad color values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c') - self.hass.block_till_done() - - # color should not have changed - state = self.hass.states.get('light.test') - 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') - self.hass.block_till_done() - - # white value should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(222, state.attributes.get('white_value')) - - # bad effect value - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white') - self.hass.block_till_done() - - # effect should not have changed - state = self.hass.states.get('light.test') - self.assertEqual('rainbow', state.attributes.get('effect')) - - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}', - 'availability_topic': 'availability-topic' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) +"""The tests for the MQTT Template light platform. + +Configuration example with all features: + +light: + platform: mqtt_template + name: mqtt_template_light_1 + state_topic: 'home/rgb1' + command_topic: 'home/rgb1/set' + command_on_template: > + on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} + command_off_template: 'off' + state_template: '{{ value.split(",")[0] }}' + brightness_template: '{{ value.split(",")[1] }}' + color_temp_template: '{{ value.split(",")[2] }}' + white_value_template: '{{ value.split(",")[3] }}' + red_template: '{{ value.split(",")[4].split("-")[0] }}' + green_template: '{{ value.split(",")[4].split("-")[1] }}' + blue_template: '{{ value.split(",")[4].split("-")[2] }}' + +If your light doesn't support brightness feature, omit `brightness_template`. + +If your light doesn't support color temp feature, omit `color_temp_template`. + +If your light doesn't support white value feature, omit `white_value_template`. + +If your light doesn't support RGB feature, omit `(red|green|blue)_template`. +""" +import unittest + +from homeassistant.setup import setup_component +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) +import homeassistant.components.light as light +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, + assert_setup_component) + + +class TestLightMQTTTemplate(unittest.TestCase): + """Test the MQTT Template light.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_fails(self): \ + # pylint: disable=invalid-name + """Test that setup fails with missing required configuration items.""" + with assert_setup_component(0, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + } + }) + self.assertIsNone(self.hass.states.get('light.test')) + + def test_state_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state change via topic.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'test_light_rgb', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + + def test_state_brightness_color_effect_temp_white_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state, bri, color, effect, color temp, white val change.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }},' + '{{ effect|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'color_temp_template': '{{ value.split(",")[2] }}', + 'white_value_template': '{{ value.split(",")[3] }}', + 'red_template': '{{ value.split(",")[4].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[4].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[4].' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[5] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,255,145,123,255-128-64,') + self.hass.block_till_done() + + 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, state.attributes.get('brightness')) + self.assertEqual(145, state.attributes.get('color_temp')) + self.assertEqual(123, state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('effect')) + + # turn the light off + fire_mqtt_message(self.hass, 'test_light_rgb', 'off') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # lower the brightness + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(100, light_state.attributes['brightness']) + + # change the color temp + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(195, light_state.attributes['color_temp']) + + # change the color + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + + # change the white value + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(134, light_state.attributes['white_value']) + + # change the effect + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,,,,41-42-43,rainbow') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual('rainbow', light_state.attributes.get('effect')) + + def test_optimistic(self): \ + # pylint: disable=invalid-name + """Test optimistic mode.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light + light.turn_on(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'on,,,,--', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # turn the light off + light.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'off', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # turn on the light with brightness, color + light.turn_on(self.hass, 'light.test', brightness=50, + rgb_color=[75, 75, 75]) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,50,,,75-75-75', payload) + + # turn on the light with color temp and white val + light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) + self.hass.block_till_done() + + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,,200,139,--', payload) + + self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # 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(50, state.attributes['brightness']) + self.assertEqual(200, state.attributes['color_temp']) + self.assertEqual(139, state.attributes['white_value']) + + def test_flash(self): \ + # pylint: disable=invalid-name + """Test flash.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ flash }}', + 'command_off_template': 'off', + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # short flash + light.turn_on(self.hass, 'light.test', flash='short') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,short', payload) + + # long flash + light.turn_on(self.hass, 'light.test', flash='long') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,long', payload) + + def test_transition(self): + """Test for transition time being sent when included.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # transition on + light.turn_on(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,10', payload) + + # transition off + light.turn_off(self.hass, 'light.test', transition=4) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('off,4', payload) + + def test_invalid_values(self): \ + # pylint: disable=invalid-name + """Test that invalid values are ignored.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }},' + '{{ effect|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'color_temp_template': '{{ value.split(",")[2] }}', + 'white_value_template': '{{ value.split(",")[3] }}', + 'red_template': '{{ value.split(",")[4].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[4].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[4].' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[5] }}', + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,255,215,222,255-255-255,rainbow') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + 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(222, state.attributes.get('white_value')) + self.assertEqual('rainbow', state.attributes.get('effect')) + + # bad state value + fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') + self.hass.block_till_done() + + # state should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # bad brightness values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255') + self.hass.block_till_done() + + # brightness should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(255, state.attributes.get('brightness')) + + # bad color temp values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255') + self.hass.block_till_done() + + # color temp should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(215, state.attributes.get('color_temp')) + + # bad color values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c') + self.hass.block_till_done() + + # color should not have changed + state = self.hass.states.get('light.test') + 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') + self.hass.block_till_done() + + # white value should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(222, state.attributes.get('white_value')) + + # bad effect value + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white') + self.hass.block_till_done() + + # effect should not have changed + state = self.hass.states.get('light.test') + self.assertEqual('rainbow', state.attributes.get('effect')) + + def test_default_availability_payload(self): + """Test availability by default payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 25f83b1d123..a6e6d3c1a85 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -254,7 +254,7 @@ def test_signal_repetitions(hass, monkeypatch): assert protocol.send_command_ack.call_count == 2 - # test if default apply to configured devcies + # test if default apply to configured devices hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + '.test1'})) diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index eef54a6c258..a1f63e45748 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -248,7 +248,7 @@ class TestLightRfxtrx(unittest.TestCase): rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES)) - # trying to add a swicth + # trying to add a switch event = rfxtrx_core.get_rfx_object('0b1100100118cdea02010f70') event.data = bytearray([0x0b, 0x11, 0x00, 0x10, 0x01, 0x18, 0xcd, 0xea, 0x01, 0x01, 0x0f, 0x70]) diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index a260d160bb5..b925b74a7f0 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -208,7 +208,7 @@ def test_set_rgb_color(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB only + # Supports RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -226,7 +226,7 @@ def test_set_rgbw_color(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGBW + # Supports RGBW color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -245,7 +245,7 @@ def test_zw098_set_color_temp(mock_openzwave): command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, warm white, cold white + # Supports RGB, warm white, cold white color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -267,7 +267,7 @@ def test_rgb_not_supported(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts color temperature only + # Supports color temperature only color_channels = MockValue(data=0x01, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -302,7 +302,7 @@ def test_rgb_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB only + # Supports RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -321,7 +321,7 @@ def test_rgbww_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Warm White + # Supports RGB, Warm White color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -340,7 +340,7 @@ def test_rgbcw_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Cold White + # Supports RGB, Cold White color_channels = MockValue(data=0x1e, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -360,7 +360,7 @@ def test_ct_value_changed(mock_openzwave): command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Cold White + # Supports RGB, Cold White color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) diff --git a/tests/components/media_player/test_mediaroom.py b/tests/components/media_player/test_mediaroom.py new file mode 100644 index 00000000000..7c7922b87be --- /dev/null +++ b/tests/components/media_player/test_mediaroom.py @@ -0,0 +1,32 @@ +"""The tests for the mediaroom media_player.""" + +import unittest + +from homeassistant.setup import setup_component +import homeassistant.components.media_player as media_player +from tests.common import ( + assert_setup_component, get_test_home_assistant) + + +class TestMediaroom(unittest.TestCase): + """Tests the Mediaroom Component.""" + + def setUp(self): + """Initialize values for this test case class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that we started.""" + self.hass.stop() + + def test_mediaroom_config(self): + """Test set up the platform with basic configuration.""" + config = { + media_player.DOMAIN: { + 'platform': 'mediaroom', + 'name': 'Living Room' + } + } + with assert_setup_component(1, media_player.DOMAIN) as result_config: + assert setup_component(self.hass, media_player.DOMAIN, config) + assert result_config[media_player.DOMAIN] diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py new file mode 100644 index 00000000000..c3753eb53b5 --- /dev/null +++ b/tests/components/media_player/test_samsungtv.py @@ -0,0 +1,271 @@ +"""Tests for samsungtv Components.""" +import unittest +from subprocess import CalledProcessError + +from asynctest import mock + +import tests.common +from homeassistant.components.media_player import SUPPORT_TURN_ON +from homeassistant.components.media_player.samsungtv import setup_platform, \ + CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ + CONF_MAC, STATE_OFF +from tests.common import MockDependency +from homeassistant.util import dt as dt_util +from datetime import timedelta + +WORKING_CONFIG = { + CONF_HOST: 'fake', + CONF_NAME: 'fake', + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: 'fake' +} + +DISCOVERY_INFO = { + 'name': 'fake', + 'model_name': 'fake', + 'host': 'fake' +} + + +class PackageException(Exception): + """Dummy Exception.""" + + +class TestSamsungTv(unittest.TestCase): + """Testing Samsungtv component.""" + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def setUp(self, samsung_mock, wol_mock): + """Setting up test environment.""" + self.hass = tests.common.get_test_home_assistant() + self.hass.start() + self.hass.block_till_done() + self.device = SamsungTVDevice(**WORKING_CONFIG) + self.device._exceptions_class = mock.Mock() + self.device._exceptions_class.UnhandledResponse = PackageException + self.device._exceptions_class.AccessDenied = PackageException + self.device._exceptions_class.ConnectionClosed = PackageException + + def tearDown(self): + """Tear down test data.""" + self.hass.stop() + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def test_setup(self, samsung_mock, wol_mock): + """Testing setup of platform.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform( + self.hass, WORKING_CONFIG, add_devices) + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def test_setup_discovery(self, samsung_mock, wol_mock): + """Testing setup of platform with discovery.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform(self.hass, {}, add_devices, + discovery_info=DISCOVERY_INFO) + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + @mock.patch( + 'homeassistant.components.media_player.samsungtv._LOGGER.warning') + def test_setup_none(self, samsung_mock, wol_mock, mocked_warn): + """Testing setup of platform with no data.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform(self.hass, {}, add_devices, + discovery_info=None) + mocked_warn.assert_called_once_with("Cannot determine device") + add_devices.assert_not_called() + + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_update_on(self, mocked_popen): + """Testing update tv on.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 0 + self.device.update() + self.assertEqual(STATE_ON, self.device._state) + + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_update_off(self, mocked_popen): + """Testing update tv off.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 1 + self.device.update() + self.assertEqual(STATE_OFF, self.device._state) + ping = mock.Mock() + ping.communicate = mock.Mock( + side_effect=CalledProcessError("BOOM", None)) + mocked_popen.return_value = ping + self.device.update() + self.assertEqual(STATE_OFF, self.device._state) + + def test_send_key(self): + """Test for send key.""" + self.device.send_key('KEY_POWER') + self.assertEqual(STATE_ON, self.device._state) + + def test_send_key_broken_pipe(self): + """Testing broken pipe Exception.""" + _remote = mock.Mock() + self.device.get_remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=BrokenPipeError("Boom")) + self.device.get_remote.return_value = _remote + self.device.send_key("HELLO") + self.assertIsNone(self.device._remote) + self.assertEqual(STATE_ON, self.device._state) + + def test_send_key_os_error(self): + """Testing broken pipe Exception.""" + _remote = mock.Mock() + self.device.get_remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=OSError("Boom")) + self.device.get_remote.return_value = _remote + self.device.send_key("HELLO") + self.assertIsNone(self.device._remote) + self.assertEqual(STATE_OFF, self.device._state) + + def test_power_off_in_progress(self): + """Test for power_off_in_progress.""" + self.assertFalse(self.device._power_off_in_progress()) + self.device._end_of_power_off = dt_util.utcnow() + timedelta( + seconds=15) + self.assertTrue(self.device._power_off_in_progress()) + + def test_name(self): + """Test for name property.""" + self.assertEqual('fake', self.device.name) + + def test_state(self): + """Test for state property.""" + self.device._state = STATE_ON + self.assertEqual(STATE_ON, self.device.state) + self.device._state = STATE_OFF + self.assertEqual(STATE_OFF, self.device.state) + + def test_is_volume_muted(self): + """Test for is_volume_muted property.""" + self.device._muted = False + self.assertFalse(self.device.is_volume_muted) + self.device._muted = True + self.assertTrue(self.device.is_volume_muted) + + def test_supported_features(self): + """Test for supported_features property.""" + self.device._mac = None + self.assertEqual(SUPPORT_SAMSUNGTV, self.device.supported_features) + self.device._mac = "fake" + self.assertEqual( + SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON, + self.device.supported_features) + + def test_turn_off(self): + """Test for turn_off.""" + self.device.send_key = mock.Mock() + _remote = mock.Mock() + _remote.close = mock.Mock() + self.get_remote = mock.Mock(return_value=_remote) + self.device._end_of_power_off = None + self.device.turn_off() + self.assertIsNotNone(self.device._end_of_power_off) + self.device.send_key.assert_called_once_with('KEY_POWER') + self.device.send_key = mock.Mock() + self.device._config['method'] = 'legacy' + self.device.turn_off() + self.device.send_key.assert_called_once_with('KEY_POWEROFF') + + @mock.patch( + 'homeassistant.components.media_player.samsungtv._LOGGER.debug') + def test_turn_off_os_error(self, mocked_debug): + """Test for turn_off with OSError.""" + _remote = mock.Mock() + _remote.close = mock.Mock(side_effect=OSError("BOOM")) + self.device.get_remote = mock.Mock(return_value=_remote) + self.device.turn_off() + mocked_debug.assert_called_once_with("Could not establish connection.") + + def test_volume_up(self): + """Test for volume_up.""" + self.device.send_key = mock.Mock() + self.device.volume_up() + self.device.send_key.assert_called_once_with("KEY_VOLUP") + + def test_volume_down(self): + """Test for volume_down.""" + self.device.send_key = mock.Mock() + self.device.volume_down() + self.device.send_key.assert_called_once_with("KEY_VOLDOWN") + + def test_mute_volume(self): + """Test for mute_volume.""" + self.device.send_key = mock.Mock() + self.device.mute_volume(True) + self.device.send_key.assert_called_once_with("KEY_MUTE") + + def test_media_play_pause(self): + """Test for media_next_track.""" + self.device.send_key = mock.Mock() + self.device._playing = False + self.device.media_play_pause() + self.device.send_key.assert_called_once_with("KEY_PLAY") + self.assertTrue(self.device._playing) + self.device.send_key = mock.Mock() + self.device.media_play_pause() + self.device.send_key.assert_called_once_with("KEY_PAUSE") + self.assertFalse(self.device._playing) + + def test_media_play(self): + """Test for media_play.""" + self.device.send_key = mock.Mock() + self.device._playing = False + self.device.media_play() + self.device.send_key.assert_called_once_with("KEY_PLAY") + self.assertTrue(self.device._playing) + + def test_media_pause(self): + """Test for media_pause.""" + self.device.send_key = mock.Mock() + self.device._playing = True + self.device.media_pause() + self.device.send_key.assert_called_once_with("KEY_PAUSE") + self.assertFalse(self.device._playing) + + def test_media_next_track(self): + """Test for media_next_track.""" + self.device.send_key = mock.Mock() + self.device.media_next_track() + self.device.send_key.assert_called_once_with("KEY_FF") + + def test_media_previous_track(self): + """Test for media_previous_track.""" + self.device.send_key = mock.Mock() + self.device.media_previous_track() + self.device.send_key.assert_called_once_with("KEY_REWIND") + + def test_turn_on(self): + """Test turn on.""" + self.device.send_key = mock.Mock() + self.device._mac = None + self.device.turn_on() + self.device.send_key.assert_called_once_with('KEY_POWERON') + self.device._wol.send_magic_packet = mock.Mock() + self.device._mac = "fake" + self.device.turn_on() + self.device._wol.send_magic_packet.assert_called_once_with("fake") diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 815204e718a..d3ebc67931f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -152,7 +152,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch('soco.discover') def test_ensure_setup_config_interface_addr(self, discover_mock, *args): - """Test a interface address config'd by the HASS config file.""" + """Test an interface address config'd by the HASS config file.""" discover_mock.return_value = {SoCoMock('192.0.2.1')} config = { @@ -172,7 +172,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.discover') def test_ensure_setup_config_advertise_addr(self, discover_mock, *args): - """Test a advertise address config'd by the HASS config file.""" + """Test an advertise address config'd by the HASS config file.""" discover_mock.return_value = {SoCoMock('192.0.2.1')} config = { diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index a8242b39f7f..2da2622e08a 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -158,7 +158,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.hass.stop() @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) - def test_ensure_setup_config(self, mocked_sountouch_device): + def test_ensure_setup_config(self, mocked_soundtouch_device): """Test setup OK with custom config.""" soundtouch.setup_platform(self.hass, default_component(), @@ -167,10 +167,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(len(all_devices), 1) self.assertEqual(all_devices[0].name, 'soundtouch') self.assertEqual(all_devices[0].config['port'], 8090) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) - def test_ensure_setup_discovery(self, mocked_sountouch_device): + def test_ensure_setup_discovery(self, mocked_soundtouch_device): """Test setup with discovery.""" new_device = {"port": "8090", "host": "192.168.1.1", @@ -184,11 +184,11 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(len(all_devices), 1) self.assertEqual(all_devices[0].config['port'], 8090) self.assertEqual(all_devices[0].config['host'], '192.168.1.1') - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) def test_ensure_setup_discovery_no_duplicate(self, - mocked_sountouch_device): + mocked_soundtouch_device): """Test setup OK if device already exists.""" soundtouch.setup_platform(self.hass, default_component(), @@ -213,20 +213,20 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock(), existing_device # Existing device ) - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_update(self, mocked_sountouch_device, mocked_status, + def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test update device state.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) self.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update() @@ -238,13 +238,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPlaying) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_media(self, mocked_sountouch_device, mocked_status, + def test_playing_media(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing media info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -261,13 +261,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusUnknown) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_unknown_media(self, mocked_sountouch_device, + def test_playing_unknown_media(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing media info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -278,13 +278,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPlayingRadio) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_radio(self, mocked_sountouch_device, mocked_status, + def test_playing_radio(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing radio info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -301,13 +301,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_volume_level(self, mocked_sountouch_device, mocked_status, + def test_get_volume_level(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test volume level.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -318,13 +318,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusStandby) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_state_off(self, mocked_sountouch_device, mocked_status, + def test_get_state_off(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test state device is off.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -335,13 +335,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPause) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_state_pause(self, mocked_sountouch_device, mocked_status, + def test_get_state_pause(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test state device is paused.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -352,25 +352,25 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_is_muted(self, mocked_sountouch_device, mocked_status, + def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test device volume is muted.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(all_devices[0].is_volume_muted, True) @mock.patch('libsoundtouch.soundtouch_device') - def test_media_commands(self, mocked_sountouch_device): + def test_media_commands(self, mocked_soundtouch_device): """Test supported media commands.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(all_devices[0].supported_features, 17853) @@ -379,7 +379,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_should_turn_off(self, mocked_sountouch_device, mocked_status, + def test_should_turn_off(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_off): """Test device is turned off.""" soundtouch.setup_platform(self.hass, @@ -387,7 +387,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].turn_off() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_power_off.call_count, 1) @@ -397,7 +397,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_should_turn_on(self, mocked_sountouch_device, mocked_status, + def test_should_turn_on(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_on): """Test device is turned on.""" soundtouch.setup_platform(self.hass, @@ -405,7 +405,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].turn_on() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_power_on.call_count, 1) @@ -415,7 +415,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_volume_up(self, mocked_sountouch_device, mocked_status, + def test_volume_up(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_up): """Test volume up.""" soundtouch.setup_platform(self.hass, @@ -423,7 +423,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].volume_up() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_volume_up.call_count, 1) @@ -433,7 +433,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_volume_down(self, mocked_sountouch_device, mocked_status, + def test_volume_down(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_down): """Test volume down.""" soundtouch.setup_platform(self.hass, @@ -441,7 +441,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].volume_down() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_volume_down.call_count, 1) @@ -451,7 +451,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_set_volume_level(self, mocked_sountouch_device, mocked_status, + def test_set_volume_level(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_set_volume): """Test set volume level.""" soundtouch.setup_platform(self.hass, @@ -459,7 +459,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].set_volume_level(0.17) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) mocked_set_volume.assert_called_with(17) @@ -469,7 +469,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_mute(self, mocked_sountouch_device, mocked_status, mocked_volume, + def test_mute(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_mute): """Test mute volume.""" soundtouch.setup_platform(self.hass, @@ -477,7 +477,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].mute_volume(None) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_mute.call_count, 1) @@ -487,7 +487,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play(self, mocked_sountouch_device, mocked_status, mocked_volume, + def test_play(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play): """Test play command.""" soundtouch.setup_platform(self.hass, @@ -495,7 +495,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_play() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_play.call_count, 1) @@ -505,15 +505,15 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_pause(self, mocked_sountouch_device, mocked_status, mocked_volume, - mocked_pause): + def test_pause(self, mocked_soundtouch_device, mocked_status, + mocked_volume, mocked_pause): """Test pause command.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_pause() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_pause.call_count, 1) @@ -523,7 +523,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_pause_play(self, mocked_sountouch_device, mocked_status, + def test_play_pause_play(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_pause): """Test play/pause.""" soundtouch.setup_platform(self.hass, @@ -531,7 +531,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_play_pause() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_play_pause.call_count, 1) @@ -542,7 +542,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_next_previous_track(self, mocked_sountouch_device, mocked_status, + def test_next_previous_track(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_next_track, mocked_previous_track): """Test next/previous track.""" @@ -550,7 +550,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].media_next_track() @@ -567,14 +567,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_media(self, mocked_sountouch_device, mocked_status, + def test_play_media(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_presets, mocked_select_preset): """Test play preset 1.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].play_media('PLAYLIST', 1) @@ -589,14 +589,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_media_url(self, mocked_sountouch_device, mocked_status, + def test_play_media_url(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_url): """Test play preset 1.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].play_media('MUSIC', "http://fqdn/file.mp3") @@ -607,7 +607,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_everywhere(self, mocked_sountouch_device, mocked_status, + def test_play_everywhere(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone): """Test play everywhere.""" soundtouch.setup_platform(self.hass, @@ -619,7 +619,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -647,7 +647,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_create_zone(self, mocked_sountouch_device, mocked_status, + def test_create_zone(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone): """Test creating a zone.""" soundtouch.setup_platform(self.hass, @@ -659,7 +659,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -689,7 +689,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, + def test_remove_zone_slave(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_remove_zone_slave): """Test adding a slave to an existing zone.""" soundtouch.setup_platform(self.hass, @@ -701,7 +701,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -731,7 +731,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, + def test_add_zone_slave(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_add_zone_slave): """Test removing a slave from a zone.""" soundtouch.setup_platform(self.hass, @@ -743,7 +743,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py index 3322f6021e7..e17241485db 100644 --- a/tests/components/media_player/test_yamaha.py +++ b/tests/components/media_player/test_yamaha.py @@ -44,7 +44,7 @@ class TestYamahaMediaPlayer(unittest.TestCase): self.hass.stop() def enable_output(self, port, enabled): - """Enable ouput on a specific port.""" + """Enable output on a specific port.""" data = { 'entity_id': 'media_player.yamaha_receiver_main_zone', 'port': port, diff --git a/tests/components/notify/test_facebook.py b/tests/components/notify/test_facebook.py index 7bc7a55869a..b94a4c38a40 100644 --- a/tests/components/notify/test_facebook.py +++ b/tests/components/notify/test_facebook.py @@ -6,7 +6,7 @@ import homeassistant.components.notify.facebook as facebook class TestFacebook(unittest.TestCase): - """Tests for Facebook notifification service.""" + """Tests for Facebook notification service.""" def setUp(self): """Set up test variables.""" diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 7c558f2803d..5ac9b3adb81 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -25,7 +25,7 @@ def create_engine_test(*args, **kwargs): @asyncio.coroutine def test_schema_update_calls(hass): - """Test that schema migrations occurr in correct order.""" + """Test that schema migrations occur in correct order.""" with patch('sqlalchemy.create_engine', new=create_engine_test), \ patch('homeassistant.components.recorder.migration._apply_update') as \ update: diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index b35b5630d60..79e2bf4ee35 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -3,13 +3,13 @@ import copy import unittest from unittest.mock import Mock -from canary.api import SensorType from homeassistant.components.canary import DATA_CANARY from homeassistant.components.sensor import canary -from homeassistant.components.sensor.canary import CanarySensor +from homeassistant.components.sensor.canary import CanarySensor, \ + SENSOR_TYPES, ATTR_AIR_QUALITY, STATE_AIR_QUALITY_NORMAL, \ + STATE_AIR_QUALITY_ABNORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL from tests.common import (get_test_home_assistant) -from tests.components.test_canary import mock_device, mock_reading, \ - mock_location +from tests.components.test_canary import mock_device, mock_location VALID_CONFIG = { "canary": { @@ -55,38 +55,33 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual(6, len(self.DEVICES)) - def test_celsius_temperature_sensor(self): - """Test temperature sensor with celsius.""" - device = mock_device(10, "Family Room") - location = mock_location("Home", True) - - data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.TEMPERATURE, 21.1234)] - - sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device) - sensor.update() - - self.assertEqual("Home Family Room Temperature", sensor.name) - self.assertEqual("sensor_canary_10_temperature", sensor.unique_id) - self.assertEqual("°C", sensor.unit_of_measurement) - self.assertEqual(21.1, sensor.state) - - def test_fahrenheit_temperature_sensor(self): + def test_temperature_sensor(self): """Test temperature sensor with fahrenheit.""" device = mock_device(10, "Family Room") location = mock_location("Home", False) data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.TEMPERATURE, 21.1567)] + data.get_reading.return_value = 21.1234 - sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) sensor.update() self.assertEqual("Home Family Room Temperature", sensor.name) - self.assertEqual("°F", sensor.unit_of_measurement) - self.assertEqual(21.2, sensor.state) + self.assertEqual("°C", sensor.unit_of_measurement) + self.assertEqual(21.12, sensor.state) + + def test_temperature_sensor_with_none_sensor_value(self): + """Test temperature sensor with fahrenheit.""" + device = mock_device(10, "Family Room") + location = mock_location("Home", False) + + data = Mock() + data.get_reading.return_value = None + + sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) + sensor.update() + + self.assertEqual(None, sensor.state) def test_humidity_sensor(self): """Test humidity sensor.""" @@ -94,28 +89,79 @@ class TestCanarySensorSetup(unittest.TestCase): location = mock_location("Home") data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.HUMIDITY, 50.4567)] + data.get_reading.return_value = 50.4567 - sensor = CanarySensor(data, SensorType.HUMIDITY, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[1], location, device) sensor.update() self.assertEqual("Home Family Room Humidity", sensor.name) self.assertEqual("%", sensor.unit_of_measurement) - self.assertEqual(50.5, sensor.state) + self.assertEqual(50.46, sensor.state) - def test_air_quality_sensor(self): + def test_air_quality_sensor_with_very_abnormal_reading(self): """Test air quality sensor.""" device = mock_device(10, "Family Room") location = mock_location("Home") data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.AIR_QUALITY, 50.4567)] + data.get_reading.return_value = 0.4 - sensor = CanarySensor(data, SensorType.AIR_QUALITY, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) sensor.update() self.assertEqual("Home Family Room Air Quality", sensor.name) - self.assertEqual("", sensor.unit_of_measurement) - self.assertEqual(50.5, sensor.state) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(0.4, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality) + + def test_air_quality_sensor_with_abnormal_reading(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 0.59 + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual("Home Family Room Air Quality", sensor.name) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(0.59, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality) + + def test_air_quality_sensor_with_normal_reading(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 1.0 + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual("Home Family Room Air Quality", sensor.name) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(1.0, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality) + + def test_air_quality_sensor_with_none_sensor_value(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = None + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual(None, sensor.state) + self.assertEqual(None, sensor.device_state_attributes) diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 86e637ab1ae..e5fca461a23 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -110,7 +110,7 @@ def test_derivative(): yield from entity.async_update() assert entity.state == STATE_UNKNOWN, \ - 'state after first update shoudl still be unknown' + 'state after first update should still be unknown' entity.telegram = { '1.0.0': MBusObject([ diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py index 00e8f2ba525..aa048f7a62e 100644 --- a/tests/components/sensor/test_file.py +++ b/tests/components/sensor/test_file.py @@ -9,7 +9,7 @@ from mock_open import MockOpen from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry class TestFileSensor(unittest.TestCase): @@ -18,6 +18,7 @@ class TestFileSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + mock_registry(self.hass) def teardown_method(self, method): """Stop everything that was started.""" diff --git a/tests/components/sensor/test_hddtemp.py b/tests/components/sensor/test_hddtemp.py index 3be35f3281c..1b65af7fd7e 100644 --- a/tests/components/sensor/test_hddtemp.py +++ b/tests/components/sensor/test_hddtemp.py @@ -1,216 +1,216 @@ -"""The tests for the hddtemp platform.""" -import socket - -import unittest -from unittest.mock import patch - -from homeassistant.setup import setup_component - -from tests.common import get_test_home_assistant - -VALID_CONFIG_MINIMAL = { - 'sensor': { - 'platform': 'hddtemp', - } -} - -VALID_CONFIG_NAME = { - 'sensor': { - 'platform': 'hddtemp', - 'name': 'FooBar', - } -} - -VALID_CONFIG_ONE_DISK = { - 'sensor': { - 'platform': 'hddtemp', - 'disks': [ - '/dev/sdd1', - ], - } -} - -VALID_CONFIG_WRONG_DISK = { - 'sensor': { - 'platform': 'hddtemp', - 'disks': [ - '/dev/sdx1', - ], - } -} - -VALID_CONFIG_MULTIPLE_DISKS = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'foobar.local', - 'disks': [ - '/dev/sda1', - '/dev/sdb1', - '/dev/sdc1', - ], - } -} - -VALID_CONFIG_HOST = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'alice.local', - } -} - -VALID_CONFIG_HOST_UNREACHABLE = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'bob.local', - } -} - - -class TelnetMock(): - """Mock class for the telnetlib.Telnet object.""" - - def __init__(self, host, port, timeout=0): - """Initialize Telnet object.""" - self.host = host - self.port = port - self.timeout = timeout - self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' + - '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' + - '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' + - '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|', - 'ascii') - - def read_all(self): - """Return sample values.""" - if self.host == 'alice.local': - raise ConnectionRefusedError - elif self.host == 'bob.local': - raise socket.gaierror - else: - return self.sample_data - return None - - -class TestHDDTempSensor(unittest.TestCase): - """Test the hddtemp sensor.""" - - def setUp(self): - """Set up things to run when tests begin.""" - self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_ONE_DISK - self.reference = {'/dev/sda1': {'device': '/dev/sda1', - 'temperature': '29', - 'unit_of_measurement': '°C', - 'model': 'WDC WD30EZRX-12DC0B0', }, - '/dev/sdb1': {'device': '/dev/sdb1', - 'temperature': '32', - 'unit_of_measurement': '°C', - 'model': 'WDC WD15EADS-11P7B2', }, - '/dev/sdc1': {'device': '/dev/sdc1', - 'temperature': '29', - 'unit_of_measurement': '°C', - 'model': 'WDC WD20EARX-22MMMB0', }, - '/dev/sdd1': {'device': '/dev/sdd1', - 'temperature': '32', - 'unit_of_measurement': '°C', - 'model': 'WDC WD15EARS-00Z5B1', }, } - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_min_config(self): - """Test minimal hddtemp configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) - - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, reference['temperature']) - self.assertEqual(state.attributes.get('device'), reference['device']) - self.assertEqual(state.attributes.get('model'), reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_rename_config(self): - """Test hddtemp configuration with different name.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) - - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.attributes.get('friendly_name'), - 'FooBar ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_one_disk(self): - """Test hddtemp one disk configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK) - - state = self.hass.states.get('sensor.hd_temperature_devsdd1') - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, reference['temperature']) - self.assertEqual(state.attributes.get('device'), reference['device']) - self.assertEqual(state.attributes.get('model'), reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_wrong_disk(self): - """Test hddtemp wrong disk configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK) - - self.assertEqual(len(self.hass.states.all()), 1) - state = self.hass.states.get('sensor.hd_temperature_devsdx1') - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + '/dev/sdx1') - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_multiple_disks(self): - """Test hddtemp multiple disk configuration.""" - assert setup_component(self.hass, - 'sensor', VALID_CONFIG_MULTIPLE_DISKS) - - for sensor in ['sensor.hd_temperature_devsda1', - 'sensor.hd_temperature_devsdb1', - 'sensor.hd_temperature_devsdc1']: - - state = self.hass.states.get(sensor) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, - reference['temperature']) - self.assertEqual(state.attributes.get('device'), - reference['device']) - self.assertEqual(state.attributes.get('model'), - reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_host_refused(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST) - self.assertEqual(len(self.hass.states.all()), 0) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_host_unreachable(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, 'sensor', - VALID_CONFIG_HOST_UNREACHABLE) - self.assertEqual(len(self.hass.states.all()), 0) +"""The tests for the hddtemp platform.""" +import socket + +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +VALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'hddtemp', + } +} + +VALID_CONFIG_NAME = { + 'sensor': { + 'platform': 'hddtemp', + 'name': 'FooBar', + } +} + +VALID_CONFIG_ONE_DISK = { + 'sensor': { + 'platform': 'hddtemp', + 'disks': [ + '/dev/sdd1', + ], + } +} + +VALID_CONFIG_WRONG_DISK = { + 'sensor': { + 'platform': 'hddtemp', + 'disks': [ + '/dev/sdx1', + ], + } +} + +VALID_CONFIG_MULTIPLE_DISKS = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'foobar.local', + 'disks': [ + '/dev/sda1', + '/dev/sdb1', + '/dev/sdc1', + ], + } +} + +VALID_CONFIG_HOST = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'alice.local', + } +} + +VALID_CONFIG_HOST_UNREACHABLE = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'bob.local', + } +} + + +class TelnetMock(): + """Mock class for the telnetlib.Telnet object.""" + + def __init__(self, host, port, timeout=0): + """Initialize Telnet object.""" + self.host = host + self.port = port + self.timeout = timeout + self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' + + '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' + + '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' + + '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|', + 'ascii') + + def read_all(self): + """Return sample values.""" + if self.host == 'alice.local': + raise ConnectionRefusedError + elif self.host == 'bob.local': + raise socket.gaierror + else: + return self.sample_data + return None + + +class TestHDDTempSensor(unittest.TestCase): + """Test the hddtemp sensor.""" + + def setUp(self): + """Set up things to run when tests begin.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG_ONE_DISK + self.reference = {'/dev/sda1': {'device': '/dev/sda1', + 'temperature': '29', + 'unit_of_measurement': '°C', + 'model': 'WDC WD30EZRX-12DC0B0', }, + '/dev/sdb1': {'device': '/dev/sdb1', + 'temperature': '32', + 'unit_of_measurement': '°C', + 'model': 'WDC WD15EADS-11P7B2', }, + '/dev/sdc1': {'device': '/dev/sdc1', + 'temperature': '29', + 'unit_of_measurement': '°C', + 'model': 'WDC WD20EARX-22MMMB0', }, + '/dev/sdd1': {'device': '/dev/sdd1', + 'temperature': '32', + 'unit_of_measurement': '°C', + 'model': 'WDC WD15EARS-00Z5B1', }, } + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_min_config(self): + """Test minimal hddtemp configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + entity = self.hass.states.all()[0].entity_id + state = self.hass.states.get(entity) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, reference['temperature']) + self.assertEqual(state.attributes.get('device'), reference['device']) + self.assertEqual(state.attributes.get('model'), reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_rename_config(self): + """Test hddtemp configuration with different name.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) + + entity = self.hass.states.all()[0].entity_id + state = self.hass.states.get(entity) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.attributes.get('friendly_name'), + 'FooBar ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_one_disk(self): + """Test hddtemp one disk configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK) + + state = self.hass.states.get('sensor.hd_temperature_devsdd1') + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, reference['temperature']) + self.assertEqual(state.attributes.get('device'), reference['device']) + self.assertEqual(state.attributes.get('model'), reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_wrong_disk(self): + """Test hddtemp wrong disk configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK) + + self.assertEqual(len(self.hass.states.all()), 1) + state = self.hass.states.get('sensor.hd_temperature_devsdx1') + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + '/dev/sdx1') + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_multiple_disks(self): + """Test hddtemp multiple disk configuration.""" + assert setup_component(self.hass, + 'sensor', VALID_CONFIG_MULTIPLE_DISKS) + + for sensor in ['sensor.hd_temperature_devsda1', + 'sensor.hd_temperature_devsdb1', + 'sensor.hd_temperature_devsdc1']: + + state = self.hass.states.get(sensor) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, + reference['temperature']) + self.assertEqual(state.attributes.get('device'), + reference['device']) + self.assertEqual(state.attributes.get('model'), + reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_host_refused(self): + """Test hddtemp if host unreachable.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST) + self.assertEqual(len(self.hass.states.all()), 0) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_host_unreachable(self): + """Test hddtemp if host unreachable.""" + assert setup_component(self.hass, 'sensor', + VALID_CONFIG_HOST_UNREACHABLE) + self.assertEqual(len(self.hass.states.all()), 0) diff --git a/tests/components/sensor/test_melissa.py b/tests/components/sensor/test_melissa.py new file mode 100644 index 00000000000..55b3e7f70f4 --- /dev/null +++ b/tests/components/sensor/test_melissa.py @@ -0,0 +1,89 @@ +"""Test for Melissa climate component.""" +import unittest +import json +from unittest.mock import Mock + +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.components.sensor import melissa +from homeassistant.components.sensor.melissa import MelissaTemperatureSensor, \ + MelissaHumiditySensor +from homeassistant.const import TEMP_CELSIUS +from tests.common import get_test_home_assistant, load_fixture + + +class TestMelissa(unittest.TestCase): + """Tests for Melissa climate.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up test variables.""" + self.hass = get_test_home_assistant() + self._serial = '12345678' + + self.api = Mock() + self.api.fetch_devices.return_value = json.loads(load_fixture( + 'melissa_fetch_devices.json' + )) + self.api.status.return_value = json.loads(load_fixture( + 'melissa_status.json' + )) + + self.api.TEMP = 'temp' + self.api.HUMIDITY = 'humidity' + device = self.api.fetch_devices()[self._serial] + self.temp = MelissaTemperatureSensor(device, self.api) + self.hum = MelissaHumiditySensor(device, self.api) + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + def test_setup_platform(self): + """Test setup_platform.""" + self.hass.data[DATA_MELISSA] = self.api + + config = {} + add_devices = Mock() + discovery_info = {} + + melissa.setup_platform(self.hass, config, add_devices, discovery_info) + + def test_name(self): + """Test name property.""" + device = self.api.fetch_devices()[self._serial] + self.assertEqual(self.temp.name, '{0} {1}'.format( + device['name'], + self.temp._type + )) + self.assertEqual(self.hum.name, '{0} {1}'.format( + device['name'], + self.hum._type + )) + + def test_state(self): + """Test state property.""" + device = self.api.status()[self._serial] + self.temp.update() + self.assertEqual(self.temp.state, device[self.api.TEMP]) + self.hum.update() + self.assertEqual(self.hum.state, device[self.api.HUMIDITY]) + + def test_unit_of_measurement(self): + """Test unit of measurement property.""" + self.assertEqual(self.temp.unit_of_measurement, TEMP_CELSIUS) + self.assertEqual(self.hum.unit_of_measurement, '%') + + def test_update(self): + """Test for update.""" + self.temp.update() + self.assertEqual(self.temp.state, 27.4) + self.hum.update() + self.assertEqual(self.hum.state, 18.7) + + def test_update_keyerror(self): + """Test for faulty update.""" + self.temp._api.status.return_value = {} + self.temp.update() + self.assertEqual(None, self.temp.state) + self.hum._api.status.return_value = {} + self.hum.update() + self.assertEqual(None, self.hum.state) diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index efcd44658c3..b23d89e3057 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -110,7 +110,7 @@ class TestSensorMQTT(unittest.TestCase): self.assertEqual('unknown', state.state) def test_setting_sensor_value_via_mqtt_json_message(self): - """Test the setting of the value via MQTT with JSON playload.""" + """Test the setting of the value via MQTT with JSON payload.""" mock_component(self.hass, 'mqtt') assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { @@ -244,7 +244,7 @@ class TestSensorMQTT(unittest.TestCase): self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) def test_setting_sensor_attribute_via_mqtt_json_message(self): - """Test the setting of attribute via MQTT with JSON playload.""" + """Test the setting of attribute via MQTT with JSON payload.""" mock_component(self.hass, 'mqtt') assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { diff --git a/tests/components/sensor/test_random.py b/tests/components/sensor/test_random.py index eeefef74c02..e04fc31af84 100644 --- a/tests/components/sensor/test_random.py +++ b/tests/components/sensor/test_random.py @@ -18,7 +18,7 @@ class TestRandomSensor(unittest.TestCase): self.hass.stop() def test_random_sensor(self): - """Test the Randowm number sensor.""" + """Test the Random number sensor.""" config = { 'sensor': { 'platform': 'random', diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py index fb31dc7c53c..0cce0ea681d 100644 --- a/tests/components/sensor/test_ring.py +++ b/tests/components/sensor/test_ring.py @@ -50,7 +50,7 @@ class TestRingSensorSetup(unittest.TestCase): @requests_mock.Mocker() def test_sensor(self, mock): - """Test the Ring senskor class and methods.""" + """Test the Ring sensor class and methods.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) mock.get('https://api.ring.com/clients_api/ring_devices', diff --git a/tests/components/sensor/test_season.py b/tests/components/sensor/test_season.py index 9dda0d2f2cb..5c071982f7f 100644 --- a/tests/components/sensor/test_season.py +++ b/tests/components/sensor/test_season.py @@ -73,7 +73,7 @@ class TestSeason(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_season_should_be_summer_northern_astonomical(self): + def test_season_should_be_summer_northern_astronomical(self): """Test that season should be summer.""" # A known day in summer summer_day = datetime(2017, 9, 3, 0, 0) @@ -91,7 +91,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SUMMER, current_season) - def test_season_should_be_autumn_northern_astonomical(self): + def test_season_should_be_autumn_northern_astronomical(self): """Test that season should be autumn.""" # A known day in autumn autumn_day = datetime(2017, 9, 23, 0, 0) @@ -109,7 +109,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_AUTUMN, current_season) - def test_season_should_be_winter_northern_astonomical(self): + def test_season_should_be_winter_northern_astronomical(self): """Test that season should be winter.""" # A known day in winter winter_day = datetime(2017, 12, 25, 0, 0) @@ -127,7 +127,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_WINTER, current_season) - def test_season_should_be_spring_northern_astonomical(self): + def test_season_should_be_spring_northern_astronomical(self): """Test that season should be spring.""" # A known day in spring spring_day = datetime(2017, 4, 1, 0, 0) @@ -145,7 +145,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SPRING, current_season) - def test_season_should_be_winter_southern_astonomical(self): + def test_season_should_be_winter_southern_astronomical(self): """Test that season should be winter.""" # A known day in winter winter_day = datetime(2017, 9, 3, 0, 0) @@ -163,7 +163,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_WINTER, current_season) - def test_season_should_be_spring_southern_astonomical(self): + def test_season_should_be_spring_southern_astronomical(self): """Test that season should be spring.""" # A known day in spring spring_day = datetime(2017, 9, 23, 0, 0) @@ -181,7 +181,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SPRING, current_season) - def test_season_should_be_summer_southern_astonomical(self): + def test_season_should_be_summer_southern_astronomical(self): """Test that season should be summer.""" # A known day in summer summer_day = datetime(2017, 12, 25, 0, 0) @@ -199,7 +199,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SUMMER, current_season) - def test_season_should_be_autumn_southern_astonomical(self): + def test_season_should_be_autumn_southern_astronomical(self): """Test that season should be spring.""" # A known day in spring autumn_day = datetime(2017, 4, 1, 0, 0) diff --git a/tests/components/sensor/test_sql.py b/tests/components/sensor/test_sql.py new file mode 100644 index 00000000000..ebf2d749e67 --- /dev/null +++ b/tests/components/sensor/test_sql.py @@ -0,0 +1,37 @@ +"""The test for the sql sensor platform.""" +import unittest + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + + +class TestSQLSensor(unittest.TestCase): + """Test the SQL sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_query(self): + """Test the SQL sensor.""" + config = { + 'sensor': { + 'platform': 'sql', + 'db_url': 'sqlite://', + 'queries': [{ + 'name': 'count_tables', + 'query': 'SELECT count(*) value FROM sqlite_master;', + 'column': 'value', + }] + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.count_tables') + self.assertEqual(state.state, '0') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index 0d2a486cb4f..a1e600860f9 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -238,7 +238,8 @@ class TestSwitchFlux(unittest.TestCase): switch.DOMAIN: { 'platform': 'flux', 'name': 'flux', - 'lights': [dev1.entity_id] + 'lights': [dev1.entity_id], + 'stop_time': '22:00' } }) turn_on_calls = mock_service( @@ -638,7 +639,8 @@ class TestSwitchFlux(unittest.TestCase): 'name': 'flux', 'lights': [dev1.entity_id], 'start_colortemp': '1000', - 'stop_colortemp': '6000' + 'stop_colortemp': '6000', + 'stop_time': '22:00' } }) turn_on_calls = mock_service( @@ -686,7 +688,8 @@ class TestSwitchFlux(unittest.TestCase): 'platform': 'flux', 'name': 'flux', 'lights': [dev1.entity_id], - 'brightness': 255 + 'brightness': 255, + 'stop_time': '22:00' } }) turn_on_calls = mock_service( diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index 2c4be648c8c..167c3bb35ac 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -1,192 +1,192 @@ -"""The tests for the wake on lan switch platform.""" -import unittest -from unittest.mock import patch - -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.components.switch as switch - -from tests.common import get_test_home_assistant, mock_service - - -TEST_STATE = None - - -def send_magic_packet(*macs, **kwargs): - """Fake call for sending magic packets.""" - return - - -def call(cmd, stdout, stderr): - """Return fake subprocess return codes.""" - if cmd[5] == 'validhostname' and TEST_STATE: - return 0 - return 2 - - -def system(): - """Fake system call to test the windows platform.""" - return 'Windows' - - -class TestWOLSwitch(unittest.TestCase): - """Test the wol switch.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_valid_hostname(self): - """Test with valid hostname.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - switch.turn_off(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - @patch('platform.system', new=system) - def test_valid_hostname_windows(self): - """Test with valid hostname on windows.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_minimal_config(self): - """Test with minimal config.""" - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - } - })) - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_broadcast_config(self): - """Test with broadcast address config.""" - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'broadcast_address': '255.255.255.255', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_off_script(self): - """Test with turn off script.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - 'turn_off': { - 'service': 'shell_command.turn_off_TARGET', - }, - } - })) - calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - assert len(calls) == 0 - - TEST_STATE = False - - switch.turn_off(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - assert len(calls) == 1 - - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - @patch('platform.system', new=system) - def test_invalid_hostname_windows(self): - """Test with invalid hostname on windows.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'invalidhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) +"""The tests for the wake on lan switch platform.""" +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + +from tests.common import get_test_home_assistant, mock_service + + +TEST_STATE = None + + +def send_magic_packet(*macs, **kwargs): + """Fake call for sending magic packets.""" + return + + +def call(cmd, stdout, stderr): + """Return fake subprocess return codes.""" + if cmd[5] == 'validhostname' and TEST_STATE: + return 0 + return 2 + + +def system(): + """Fake system call to test the windows platform.""" + return 'Windows' + + +class TestWOLSwitch(unittest.TestCase): + """Test the wol switch.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_valid_hostname(self): + """Test with valid hostname.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + @patch('platform.system', new=system) + def test_valid_hostname_windows(self): + """Test with valid hostname on windows.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_minimal_config(self): + """Test with minimal config.""" + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + } + })) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_broadcast_config(self): + """Test with broadcast address config.""" + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'broadcast_address': '255.255.255.255', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_off_script(self): + """Test with turn off script.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + 'turn_off': { + 'service': 'shell_command.turn_off_TARGET', + }, + } + })) + calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + assert len(calls) == 0 + + TEST_STATE = False + + switch.turn_off(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + assert len(calls) == 1 + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + @patch('platform.system', new=system) + def test_invalid_hostname_windows(self): + """Test with invalid hostname on windows.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'invalidhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index a94e5747483..d9eb33be37d 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -107,7 +107,7 @@ class TestAlert(unittest.TestCase): self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) def test_hidden(self): - """Test entity hidding.""" + """Test entity hiding.""" assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertTrue(hidden) diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py index 67122813fb7..2c496c26e11 100644 --- a/tests/components/test_canary.py +++ b/tests/components/test_canary.py @@ -17,12 +17,12 @@ def mock_device(device_id, name, is_online=True): return device -def mock_location(name, is_celsius=True, devices=[]): +def mock_location(name, is_celsius=True, devices=None): """Mock Canary Location class.""" location = MagicMock() type(location).name = PropertyMock(return_value=name) type(location).is_celsius = PropertyMock(return_value=is_celsius) - type(location).devices = PropertyMock(return_value=devices) + type(location).devices = PropertyMock(return_value=devices or []) return location diff --git a/tests/components/test_dialogflow.py b/tests/components/test_dialogflow.py index a52c841e0cc..0acf0833543 100644 --- a/tests/components/test_dialogflow.py +++ b/tests/components/test_dialogflow.py @@ -435,7 +435,7 @@ class TestDialogflow(unittest.TestCase): self.assertEqual("virgo", call.data.get("hello")) def test_intent_with_no_action(self): - """Test a intent with no defined action.""" + """Test an intent with no defined action.""" data = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, @@ -480,7 +480,7 @@ class TestDialogflow(unittest.TestCase): "You have not defined an action in your Dialogflow intent.", text) def test_intent_with_unknown_action(self): - """Test a intent with an action not defined in the conf.""" + """Test an intent with an action not defined in the conf.""" data = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py index 9cc706b5690..5a5fdffd5a3 100644 --- a/tests/components/test_ffmpeg.py +++ b/tests/components/test_ffmpeg.py @@ -97,7 +97,7 @@ def test_setup_component_test_register_no_startup(hass): @asyncio.coroutine -def test_setup_component_test_servcie_start(hass): +def test_setup_component_test_service_start(hass): """Setup ffmpeg component test service start.""" with assert_setup_component(2): yield from async_setup_component( @@ -113,7 +113,7 @@ def test_setup_component_test_servcie_start(hass): @asyncio.coroutine -def test_setup_component_test_servcie_stop(hass): +def test_setup_component_test_service_stop(hass): """Setup ffmpeg component test service stop.""" with assert_setup_component(2): yield from async_setup_component( @@ -129,7 +129,7 @@ def test_setup_component_test_servcie_stop(hass): @asyncio.coroutine -def test_setup_component_test_servcie_restart(hass): +def test_setup_component_test_service_restart(hass): """Setup ffmpeg component test service restart.""" with assert_setup_component(2): yield from async_setup_component( @@ -146,7 +146,7 @@ def test_setup_component_test_servcie_restart(hass): @asyncio.coroutine -def test_setup_component_test_servcie_start_with_entity(hass): +def test_setup_component_test_service_start_with_entity(hass): """Setup ffmpeg component test service start.""" with assert_setup_component(2): yield from async_setup_component( diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index fb869569670..8fb017309de 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -211,7 +211,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): @asyncio.coroutine def test_service_register(hassio_env, hass): - """Check if service will be settup.""" + """Check if service will be setup.""" assert (yield from async_setup_component(hass, 'hassio', {})) assert hass.services.has_service('hassio', 'addon_start') assert hass.services.has_service('hassio', 'addon_stop') diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py index b8568f3aaba..30129ec7998 100644 --- a/tests/components/test_hue.py +++ b/tests/components/test_hue.py @@ -36,7 +36,7 @@ class TestSetup(unittest.TestCase): self.assertTrue(setup_component( self.hass, hue.DOMAIN, {})) mock_phue.Bridge.assert_not_called() - self.assertEquals({}, self.hass.data[hue.DOMAIN]) + self.assertEqual({}, self.hass.data[hue.DOMAIN]) @MockDependency('phue') def test_setup_with_host(self, mock_phue): @@ -59,7 +59,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '127.0.0.1'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_setup_with_phue_conf(self, mock_phue): @@ -86,7 +86,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '127.0.0.1'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_setup_with_multiple_hosts(self, mock_phue): @@ -122,7 +122,7 @@ class TestSetup(unittest.TestCase): ], any_order=True) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(2, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(2, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_bridge_discovered(self, mock_phue): @@ -145,7 +145,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '192.168.0.10'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_bridge_configure_and_discovered(self, mock_phue): @@ -175,7 +175,7 @@ class TestSetup(unittest.TestCase): mock_load.assert_has_calls(calls_to_mock_load) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) # Then we discover the same bridge hue.bridge_discovered(self.hass, mock_service, discovery_info) @@ -189,7 +189,7 @@ class TestSetup(unittest.TestCase): # Still only one self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) class TestHueBridge(unittest.TestCase): diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index d768136592e..4d12e436c02 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -1,15 +1,10 @@ """The tests for the InfluxDB component.""" -import unittest import datetime +import unittest from unittest import mock -from datetime import timedelta -from unittest.mock import MagicMock - import influxdb as influx_client -from homeassistant.util import dt as dt_util -from homeassistant import core as ha from homeassistant.setup import setup_component import homeassistant.components.influxdb as influxdb from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, \ @@ -169,6 +164,8 @@ class TestInfluxDB(unittest.TestCase): body[0]['fields']['value'] = out[1] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -203,6 +200,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -212,18 +210,6 @@ class TestInfluxDB(unittest.TestCase): ) mock_client.return_value.write_points.reset_mock() - def test_event_listener_fail_write(self, mock_client): - """Test the event listener for write failures.""" - self._setup() - - state = mock.MagicMock( - state=1, domain='fake', entity_id='fake.entity-id', - object_id='entity', attributes={}) - event = mock.MagicMock(data={'new_state': state}, time_fired=12345) - mock_client.return_value.write_points.side_effect = \ - influx_client.exceptions.InfluxDBClientError('foo') - self.handler_method(event) - def test_event_listener_states(self, mock_client): """Test the event listener against ignored states.""" self._setup() @@ -245,6 +231,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if state_state == 1: self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -278,6 +265,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -312,6 +300,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if domain == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -356,6 +345,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'included': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -401,6 +391,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if domain == 'fake': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -456,6 +447,7 @@ class TestInfluxDB(unittest.TestCase): body[0]['fields']['value'] = out[1] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -498,6 +490,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -543,6 +536,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -588,6 +582,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -648,6 +643,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -659,7 +655,16 @@ class TestInfluxDB(unittest.TestCase): def test_scheduled_write(self, mock_client): """Test the event listener to retry after write failures.""" - self._setup(max_retries=1) + config = { + 'influxdb': { + 'host': 'host', + 'username': 'user', + 'password': 'pass', + 'max_retries': 1 + } + } + assert setup_component(self.hass, influxdb.DOMAIN, config) + self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] state = mock.MagicMock( state=1, domain='fake', entity_id='entity.id', object_id='entity', @@ -668,152 +673,47 @@ class TestInfluxDB(unittest.TestCase): mock_client.return_value.write_points.side_effect = \ IOError('foo') - start = dt_util.utcnow() - - self.handler_method(event) + # Write fails + with mock.patch.object(influxdb.time, 'sleep') as mock_sleep: + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + assert mock_sleep.called json_data = mock_client.return_value.write_points.call_args[0][0] - self.assertEqual(mock_client.return_value.write_points.call_count, 1) - - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() self.assertEqual(mock_client.return_value.write_points.call_count, 2) mock_client.return_value.write_points.assert_called_with(json_data) - shifted_time = shifted_time + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_client.return_value.write_points.call_count, 2) + # Write works again + mock_client.return_value.write_points.side_effect = None + with mock.patch.object(influxdb.time, 'sleep') as mock_sleep: + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + assert not mock_sleep.called + self.assertEqual(mock_client.return_value.write_points.call_count, 3) + def test_queue_backlog_full(self, mock_client): + """Test the event listener to drop old events.""" + self._setup() -class TestRetryOnErrorDecorator(unittest.TestCase): - """Test the RetryOnError decorator.""" + state = mock.MagicMock( + state=1, domain='fake', entity_id='entity.id', object_id='entity', + attributes={}) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + monotonic_time = 0 - def tearDown(self): - """Clear data.""" - self.hass.stop() + def fast_monotonic(): + """Monotonic time that ticks fast enough to cause a timeout.""" + nonlocal monotonic_time + monotonic_time += 60 + return monotonic_time - def test_no_retry(self): - """Test that it does not retry if configured.""" - mock_method = MagicMock() - wrapped = influxdb.RetryOnError(self.hass)(mock_method) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) + with mock.patch('homeassistant.components.influxdb.time.monotonic', + new=fast_monotonic): + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() - mock_method.side_effect = Exception() - self.assertRaises(Exception, wrapped, 1, 2, test=3) - self.assertEqual(mock_method.call_count, 2) - mock_method.assert_called_with(1, 2, test=3) + self.assertEqual( + mock_client.return_value.write_points.call_count, 0 + ) - def test_single_retry(self): - """Test that retry stops after a single try if configured.""" - mock_method = MagicMock() - retryer = influxdb.RetryOnError(self.hass, retry_limit=1) - wrapped = retryer(mock_method) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, 1) - - mock_method.side_effect = Exception() - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 2) - mock_method.assert_called_with(1, 2, test=3) - - for cnt in range(3): - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, 3) - mock_method.assert_called_with(1, 2, test=3) - - def test_multi_retry(self): - """Test that multiple retries work.""" - mock_method = MagicMock() - retryer = influxdb.RetryOnError(self.hass, retry_limit=4) - wrapped = retryer(mock_method) - mock_method.side_effect = Exception() - - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - - for cnt in range(3): - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, cnt + 2) - mock_method.assert_called_with(1, 2, test=3) - - def test_max_queue(self): - """Test the maximum queue length.""" - # make a wrapped method - mock_method = MagicMock() - retryer = influxdb.RetryOnError( - self.hass, retry_limit=4, queue_limit=3) - wrapped = retryer(mock_method) - mock_method.side_effect = Exception() - - # call it once, call fails, queue fills to 1 - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - self.assertEqual(len(wrapped._retry_queue), 1) - - # two more calls that failed. queue is 3 - wrapped(1, 2, test=3) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 3) - self.assertEqual(len(wrapped._retry_queue), 3) - - # another call, queue gets limited to 3 - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 4) - self.assertEqual(len(wrapped._retry_queue), 3) - - # time passes - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - - # only the three queued calls where repeated - self.assertEqual(mock_method.call_count, 7) - self.assertEqual(len(wrapped._retry_queue), 3) - - # another call, queue stays limited - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 8) - self.assertEqual(len(wrapped._retry_queue), 3) - - # disable the side effect - mock_method.side_effect = None - - # time passes, all calls should succeed - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - - # three queued calls succeeded, queue empty. - self.assertEqual(mock_method.call_count, 11) - self.assertEqual(len(wrapped._retry_queue), 0) + mock_client.return_value.write_points.reset_mock() diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 06ba8a57508..dde141b6495 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,4 +1,4 @@ -"""The testd for Core components.""" +"""The tests for Core components.""" # pylint: disable=protected-access import asyncio import unittest diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py index be22e1122ea..c288375ec8f 100644 --- a/tests/components/test_input_text.py +++ b/tests/components/test_input_text.py @@ -64,6 +64,41 @@ class TestInputText(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual('testing', str(state.state)) + def test_mode(self): + """Test mode settings.""" + self.assertTrue( + setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_default_text': { + 'initial': 'test', + 'min': 3, + 'max': 10, + }, + 'test_explicit_text': { + 'initial': 'test', + 'min': 3, + 'max': 10, + 'mode': 'text', + }, + 'test_explicit_password': { + 'initial': 'test', + 'min': 3, + 'max': 10, + 'mode': 'password', + }, + }})) + + state = self.hass.states.get('input_text.test_default_text') + assert state + self.assertEqual('text', state.attributes['mode']) + + state = self.hass.states.get('input_text.test_explicit_text') + assert state + self.assertEqual('text', state.attributes['mode']) + + state = self.hass.states.get('input_text.test_explicit_password') + assert state + self.assertEqual('password', state.attributes['mode']) + @asyncio.coroutine def test_restore_state(hass): diff --git a/tests/components/test_melissa.py b/tests/components/test_melissa.py new file mode 100644 index 00000000000..e39ceb1add1 --- /dev/null +++ b/tests/components/test_melissa.py @@ -0,0 +1,38 @@ +"""The test for the Melissa Climate component.""" +import unittest +from tests.common import get_test_home_assistant, MockDependency + +from homeassistant.components import melissa + +VALID_CONFIG = { + "melissa": { + "username": "********", + "password": "********", + } +} + + +class TestMelissa(unittest.TestCase): + """Test the Melissa component.""" + + def setUp(self): # pylint: disable=invalid-name + """Initialize the values for this test class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @MockDependency("melissa") + def test_setup(self, mocked_melissa): + """Test setting up the Melissa component.""" + melissa.setup(self.hass, self.config) + + mocked_melissa.Melissa.assert_called_with( + username="********", password="********") + self.assertIn(melissa.DATA_MELISSA, self.hass.data) + self.assertIsInstance( + self.hass.data[melissa.DATA_MELISSA], type( + mocked_melissa.Melissa()) + ) diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py index 91a8b326bf9..2b0f249f4c9 100644 --- a/tests/components/test_nuheat.py +++ b/tests/components/test_nuheat.py @@ -35,11 +35,11 @@ class TestNuHeat(unittest.TestCase): mocked_nuheat.NuHeat.assert_called_with("warm", "feet") self.assertIn(nuheat.DOMAIN, self.hass.data) - self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN])) + self.assertEqual(2, len(self.hass.data[nuheat.DOMAIN])) self.assertIsInstance( self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) ) - self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123") + self.assertEqual(self.hass.data[nuheat.DOMAIN][1], "thermostat123") mocked_load.assert_called_with( self.hass, "climate", nuheat.DOMAIN, {}, self.config diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 805d73e1820..ef702b96f4b 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -11,11 +11,11 @@ from tests.common import get_test_home_assistant class TestPanelIframe(unittest.TestCase): """Test the panel_iframe component.""" - def setup_method(self, method): + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def teardown_method(self, method): + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -50,6 +50,11 @@ class TestPanelIframe(unittest.TestCase): 'title': 'Weather', 'url': 'https://www.wunderground.com/us/ca/san-diego', }, + 'api': { + 'icon': 'mdi:weather', + 'title': 'Api', + 'url': '/api', + }, }, }) @@ -72,3 +77,12 @@ class TestPanelIframe(unittest.TestCase): 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'weather', } + + assert panels.get('api').to_response(self.hass, None) == { + 'component_name': 'iframe', + 'config': {'url': '/api'}, + 'icon': 'mdi:weather', + 'title': 'Api', + 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', + 'url_path': 'api', + } diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py index 7bdd44136e8..010136ee0e7 100644 --- a/tests/components/test_pilight.py +++ b/tests/components/test_pilight.py @@ -304,7 +304,7 @@ class TestPilight(unittest.TestCase): with assert_setup_component(4): whitelist = { 'protocol': [PilightDaemonSim.test_message['protocol'], - 'other_protocoll'], + 'other_protocol'], 'id': [PilightDaemonSim.test_message['message']['id']]} self.assertTrue(setup_component( self.hass, pilight.DOMAIN, @@ -330,7 +330,7 @@ class TestPilight(unittest.TestCase): """Check whitelist filter with unmatched data, should not work.""" with assert_setup_component(4): whitelist = { - 'protocol': ['wrong_protocoll'], + 'protocol': ['wrong_protocol'], 'id': [PilightDaemonSim.test_message['message']['id']]} self.assertTrue(setup_component( self.hass, pilight.DOMAIN, diff --git a/tests/components/test_plant.py b/tests/components/test_plant.py index f5a042ac8c1..ee1372509d9 100644 --- a/tests/components/test_plant.py +++ b/tests/components/test_plant.py @@ -101,11 +101,11 @@ class TestPlant(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'us/cm'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_PROBLEM, state.state) - self.assertEquals(5, state.attributes[plant.READING_MOISTURE]) + self.assertEqual(STATE_PROBLEM, state.state) + self.assertEqual(5, state.attributes[plant.READING_MOISTURE]) @pytest.mark.skipif(plant.ENABLE_LOAD_HISTORY is False, - reason="tests for loading from DB are instable, thus" + reason="tests for loading from DB are unstable, thus" "this feature is turned of until tests become" "stable") def test_load_from_db(self): @@ -132,10 +132,10 @@ class TestPlant(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_UNKNOWN, state.state) + self.assertEqual(STATE_UNKNOWN, state.state) max_brightness = state.attributes.get( plant.ATTR_MAX_BRIGHTNESS_HISTORY) - self.assertEquals(30, max_brightness) + self.assertEqual(30, max_brightness) def test_brightness_history(self): """Test the min_brightness check.""" @@ -149,19 +149,19 @@ class TestPlant(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_PROBLEM, state.state) + self.assertEqual(STATE_PROBLEM, state.state) self.hass.states.set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_OK, state.state) + self.assertEqual(STATE_OK, state.state) self.hass.states.set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_OK, state.state) + self.assertEqual(STATE_OK, state.state) class TestDailyHistory(unittest.TestCase): @@ -195,4 +195,4 @@ class TestDailyHistory(unittest.TestCase): for i in range(len(days)): dh.add_measurement(values[i], days[i]) - self.assertEquals(max_values[i], dh.max) + self.assertEqual(max_values[i], dh.max) diff --git a/tests/components/test_remember_the_milk.py b/tests/components/test_remember_the_milk.py index 1b6619aca9c..d9db61efd40 100644 --- a/tests/components/test_remember_the_milk.py +++ b/tests/components/test_remember_the_milk.py @@ -54,7 +54,7 @@ class TestConfiguration(unittest.TestCase): def test_invalid_data(self): """Test starts with invalid data and should not raise an exception.""" with patch("builtins.open", - mock_open(read_data='random charachters')),\ + mock_open(read_data='random characters')),\ patch("os.path.isfile", Mock(return_value=True)): config = rtm.RememberTheMilkConfiguration(self.hass) self.assertIsNotNone(config) diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py index e7907fc6b54..9f6573920ca 100644 --- a/tests/components/test_rflink.py +++ b/tests/components/test_rflink.py @@ -47,7 +47,7 @@ def mock_rflink(hass, config, domain, monkeypatch, failures=None, 'rflink.protocol.create_rflink_connection', mock_create) - # verify instanstiation of component with given config + # verify instantiation of component with given config with assert_setup_component(platform_count, domain): yield from async_setup_component(hass, domain, config) diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py index 60eb2530ea1..8b16b5519e9 100644 --- a/tests/components/test_rss_feed_template.py +++ b/tests/components/test_rss_feed_template.py @@ -25,7 +25,7 @@ def mock_http_client(loop, hass, test_client): @asyncio.coroutine -def test_get_noexistant_feed(mock_http_client): +def test_get_nonexistant_feed(mock_http_client): """Test if we can retrieve the correct rss feed.""" resp = yield from mock_http_client.get('/api/rss_template/otherfeed') assert resp.status == 404 diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index a3e7d662483..d119c60dba2 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import pytest +from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log @@ -13,7 +14,7 @@ _LOGGER = logging.getLogger('test_logger') @pytest.fixture(autouse=True) @asyncio.coroutine -def setup_test_case(hass): +def setup_test_case(hass, test_client): """Setup system_log component before test case.""" config = {'system_log': {'max_entries': 2}} yield from async_setup_component(hass, system_log.DOMAIN, config) @@ -34,7 +35,7 @@ def get_error_log(hass, test_client, expected_count): def _generate_and_log_exception(exception, log): try: raise Exception(exception) - except: # pylint: disable=bare-except + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception(log) @@ -85,6 +86,25 @@ def test_error(hass, test_client): assert_log(log, '', 'error message', 'ERROR') +@asyncio.coroutine +def test_error_posted_as_event(hass, test_client): + """Test that error are posted as events.""" + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert_log(events[0].data, '', 'error message', 'ERROR') + + @asyncio.coroutine def test_critical(hass, test_client): """Test that critical are logged and retrieved correctly.""" @@ -189,10 +209,10 @@ def log_error_from_test_path(path): @asyncio.coroutine def test_homeassistant_path(hass, test_client): """Test error logged from homeassistant path.""" - log_error_from_test_path('venv_path/homeassistant/component/component.py') - 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] assert log['source'] == 'component/component.py' @@ -200,9 +220,8 @@ def test_homeassistant_path(hass, test_client): @asyncio.coroutine def test_config_path(hass, test_client): """Test error logged from config path.""" - log_error_from_test_path('config/custom_component/test.py') - 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] assert log['source'] == 'custom_component/test.py' @@ -210,9 +229,8 @@ def test_config_path(hass, test_client): @asyncio.coroutine def test_netdisco_path(hass, test_client): """Test error logged from netdisco path.""" - log_error_from_test_path('venv_path/netdisco/disco_component.py') - 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] assert log['source'] == 'disco_component.py' diff --git a/tests/components/test_weblink.py b/tests/components/test_weblink.py index e8768342db9..249e81d37af 100644 --- a/tests/components/test_weblink.py +++ b/tests/components/test_weblink.py @@ -26,6 +26,71 @@ class TestComponentWeblink(unittest.TestCase): } })) + def test_bad_config_relative_url(self): + """Test if new entity is created.""" + self.assertFalse(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My router', + weblink.CONF_URL: '../states/group.bla' + }, + ], + } + })) + + def test_bad_config_relative_file(self): + """Test if new entity is created.""" + self.assertFalse(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My group', + weblink.CONF_URL: 'group.bla' + }, + ], + } + })) + + def test_good_config_absolute_path(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My second URL', + weblink.CONF_URL: '/states/group.bla' + }, + ], + } + })) + + def test_good_config_path_short(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My third URL', + weblink.CONF_URL: '/states' + }, + ], + } + })) + + def test_good_config_path_directory(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My last URL', + weblink.CONF_URL: '/states/bla/' + }, + ], + } + })) + def test_entities_get_created(self): """Test if new entity is created.""" self.assertTrue(setup_component(self.hass, weblink.DOMAIN, { diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py index e08229631cf..5b4ef4dcf53 100644 --- a/tests/components/tts/test_yandextts.py +++ b/tests/components/tts/test_yandextts.py @@ -224,7 +224,7 @@ class TestTTSYandexPlatform(object): assert len(calls) == 0 - def test_service_say_specifed_speaker(self, aioclient_mock): + def test_service_say_specified_speaker(self, aioclient_mock): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) @@ -259,7 +259,7 @@ class TestTTSYandexPlatform(object): assert len(aioclient_mock.mock_calls) == 1 assert len(calls) == 1 - def test_service_say_specifed_emotion(self, aioclient_mock): + def test_service_say_specified_emotion(self, aioclient_mock): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index e548efb0eb2..828385b9ded 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -198,7 +198,7 @@ def test_device_entity(hass, mock_openzwave): yield from hass.async_block_till_done() assert not device.should_poll - assert device.unique_id == "ZWAVE-10-11" + assert device.unique_id == "10-11" assert device.name == 'Mock Node Sensor' assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 @@ -899,7 +899,7 @@ class TestZWaveServices(unittest.TestCase): assert value.label == "New Label" def test_set_poll_intensity_enable(self): - """Test zwave set_poll_intensity service, succsessful set.""" + """Test zwave set_poll_intensity service, successful set.""" node = MockNode(node_id=14) value = MockValue(index=12, value_id=123456, poll_intensity=0) node.values = {123456: value} diff --git a/tests/fixtures/melissa_cur_settings.json b/tests/fixtures/melissa_cur_settings.json new file mode 100644 index 00000000000..9d7fb615330 --- /dev/null +++ b/tests/fixtures/melissa_cur_settings.json @@ -0,0 +1,28 @@ +{ + "controller": { + "id": 1, + "user_id": 1, + "serial_number": "12345678", + "mac": "12345678", + "firmware_version": "V1SHTHF", + "name": "Melissa 12345678", + "type": "melissa", + "room_id": null, + "created": "2016-07-06 18:59:46", + "deleted_at": null, + "online": true, + "_relation": { + "command_log": { + "state": 1, + "mode": 2, + "temp": 16, + "fan": 1 + } + } + }, + "_links": { + "self": { + "href": "/v1/controllers/12345678" + } + } +} diff --git a/tests/fixtures/melissa_fetch_devices.json b/tests/fixtures/melissa_fetch_devices.json new file mode 100644 index 00000000000..4b106a613f7 --- /dev/null +++ b/tests/fixtures/melissa_fetch_devices.json @@ -0,0 +1,27 @@ +{ + "12345678": { + "user_id": 1, + "serial_number": "12345678", + "mac": "12345678", + "firmware_version": "V1SHTHF", + "name": "Melissa 12345678", + "type": "melissa", + "room_id": null, + "created": "2016-07-06 18:59:46", + "id": 1, + "online": true, + "brand_id": 1, + "controller_log": { + "temp": 27.4, + "created": "2018-01-08T21:01:14.281Z", + "raw_temperature": 28928, + "humidity": 18.7, + "raw_humidity": 12946 + }, + "_links": { + "self": { + "href": "/v1/controllers" + } + } + } +} diff --git a/tests/fixtures/melissa_status.json b/tests/fixtures/melissa_status.json new file mode 100644 index 00000000000..ac240b3df12 --- /dev/null +++ b/tests/fixtures/melissa_status.json @@ -0,0 +1,8 @@ +{ + "12345678": { + "temp": 27.4, + "raw_temperature": 28928, + "humidity": 18.7, + "raw_humidity": 12946 + } +} diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index e413fc145ca..4211e3da31b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -232,8 +232,8 @@ def test_async_schedule_update_ha_state(hass): @asyncio.coroutine -def test_async_pararell_updates_with_zero(hass): - """Test pararell updates with 0 (disabled).""" +def test_async_parallel_updates_with_zero(hass): + """Test parallel updates with 0 (disabled).""" updates = [] test_lock = asyncio.Event(loop=hass.loop) @@ -269,11 +269,11 @@ def test_async_pararell_updates_with_zero(hass): @asyncio.coroutine -def test_async_pararell_updates_with_one(hass): - """Test pararell updates with 1 (sequential).""" +def test_async_parallel_updates_with_one(hass): + """Test parallel updates with 1 (sequential).""" updates = [] test_lock = asyncio.Lock(loop=hass.loop) - test_semephore = asyncio.Semaphore(1, loop=hass.loop) + test_semaphore = asyncio.Semaphore(1, loop=hass.loop) yield from test_lock.acquire() @@ -284,7 +284,7 @@ def test_async_pararell_updates_with_one(hass): self.entity_id = entity_id self.hass = hass self._count = count - self.parallel_updates = test_semephore + self.parallel_updates = test_semaphore @asyncio.coroutine def async_update(self): @@ -332,11 +332,11 @@ def test_async_pararell_updates_with_one(hass): @asyncio.coroutine -def test_async_pararell_updates_with_two(hass): - """Test pararell updates with 2 (pararell).""" +def test_async_parallel_updates_with_two(hass): + """Test parallel updates with 2 (parallel).""" updates = [] test_lock = asyncio.Lock(loop=hass.loop) - test_semephore = asyncio.Semaphore(2, loop=hass.loop) + test_semaphore = asyncio.Semaphore(2, loop=hass.loop) yield from test_lock.acquire() @@ -347,7 +347,7 @@ def test_async_pararell_updates_with_two(hass): self.entity_id = entity_id self.hass = hass self._count = count - self.parallel_updates = test_semephore + self.parallel_updates = test_semaphore @asyncio.coroutine def async_update(self): diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3109ea776bc..ef92da3172b 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -4,67 +4,27 @@ import asyncio from collections import OrderedDict import logging import unittest -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import patch, Mock from datetime import timedelta import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group -from homeassistant.helpers.entity import Entity, generate_entity_id -from homeassistant.helpers.entity_component import ( - EntityComponent, DEFAULT_SCAN_INTERVAL, SLOW_SETUP_WARNING) -from homeassistant.helpers import entity_component +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import setup_component from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, - mock_coro, async_fire_time_changed) + get_test_home_assistant, MockPlatform, MockModule, mock_coro, + async_fire_time_changed, MockEntity) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" -class EntityTest(Entity): - """Test for the Entity component.""" - - def __init__(self, **values): - """Initialize an entity.""" - self._values = values - - if 'entity_id' in values: - self.entity_id = values['entity_id'] - - @property - def name(self): - """Return the name of the entity.""" - return self._handle('name') - - @property - def should_poll(self): - """Return the ste of the polling.""" - return self._handle('should_poll') - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._handle('unique_id') - - @property - def available(self): - """Return True if entity is available.""" - return self._handle('available') - - def _handle(self, attr): - """Helper for the attributes.""" - if attr in self._values: - return self._values[attr] - return getattr(super(), attr) - - class TestHelpersEntityComponent(unittest.TestCase): """Test homeassistant.helpers.entity_component module.""" @@ -85,7 +45,7 @@ class TestHelpersEntityComponent(unittest.TestCase): # No group after setup assert len(self.hass.states.entity_ids()) == 0 - component.add_entities([EntityTest()]) + component.add_entities([MockEntity()]) self.hass.block_till_done() # group exists @@ -98,7 +58,7 @@ class TestHelpersEntityComponent(unittest.TestCase): ('test_domain.unnamed_device',) # group extended - component.add_entities([EntityTest(name='goodbye')]) + component.add_entities([MockEntity(name='goodbye')]) self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 3 @@ -108,175 +68,6 @@ class TestHelpersEntityComponent(unittest.TestCase): assert group.attributes.get('entity_id') == \ ('test_domain.goodbye', 'test_domain.unnamed_device') - def test_polling_only_updates_entities_it_should_poll(self): - """Test the polling of only updated entities.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) - - no_poll_ent = EntityTest(should_poll=False) - no_poll_ent.async_update = Mock() - poll_ent = EntityTest(should_poll=True) - poll_ent.async_update = Mock() - - component.add_entities([no_poll_ent, poll_ent]) - - no_poll_ent.async_update.reset_mock() - poll_ent.async_update.reset_mock() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert not no_poll_ent.async_update.called - assert poll_ent.async_update.called - - def test_polling_updates_entities_with_exception(self): - """Test the updated entities that not break with a exception.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) - - update_ok = [] - update_err = [] - - def update_mock(): - """Mock normal update.""" - update_ok.append(None) - - def update_mock_err(): - """Mock error update.""" - update_err.append(None) - raise AssertionError("Fake error update") - - ent1 = EntityTest(should_poll=True) - ent1.update = update_mock_err - ent2 = EntityTest(should_poll=True) - ent2.update = update_mock - ent3 = EntityTest(should_poll=True) - ent3.update = update_mock - ent4 = EntityTest(should_poll=True) - ent4.update = update_mock - - component.add_entities([ent1, ent2, ent3, ent4]) - - update_ok.clear() - update_err.clear() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert len(update_ok) == 3 - assert len(update_err) == 1 - - def test_update_state_adds_entities(self): - """Test if updating poll entities cause an entity to be added works.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent1 = EntityTest() - ent2 = EntityTest(should_poll=True) - - component.add_entities([ent2]) - assert 1 == len(self.hass.states.entity_ids()) - ent2.update = lambda *_: component.add_entities([ent1]) - - fire_time_changed( - self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL - ) - self.hass.block_till_done() - - assert 2 == len(self.hass.states.entity_ids()) - - def test_update_state_adds_entities_with_update_befor_add_true(self): - """Test if call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = EntityTest() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], True) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert ent.update.called - - def test_update_state_adds_entities_with_update_befor_add_false(self): - """Test if not call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = EntityTest() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], False) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert not ent.update.called - - def test_not_adding_duplicate_entities(self): - """Test for not adding duplicate entities.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 0 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - def test_not_assigning_entity_id_if_prescribes_one(self): - """Test for not assigning an entity ID.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 'hello.world' not in self.hass.states.entity_ids() - - component.add_entities([EntityTest(entity_id='hello.world')]) - - assert 'hello.world' in self.hass.states.entity_ids() - - def test_extract_from_service_returns_all_if_no_entity_id(self): - """Test the extraction of everything from service.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - component.add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2'), - ]) - - call = ha.ServiceCall('test', 'service') - - assert ['test_domain.test_1', 'test_domain.test_2'] == \ - sorted(ent.entity_id for ent in - component.extract_from_service(call)) - - def test_extract_from_service_filter_out_non_existing_entities(self): - """Test the extraction of non existing entities from service.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - component.add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2'), - ]) - - call = ha.ServiceCall('test', 'service', { - 'entity_id': ['test_domain.test_2', 'test_domain.non_exist'] - }) - - assert ['test_domain.test_2'] == \ - [ent.entity_id for ent in component.extract_from_service(call)] - - def test_extract_from_service_no_group_expand(self): - """Test not expanding a group.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - test_group = group.Group.create_group( - self.hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) - component.add_entities([test_group]) - - call = ha.ServiceCall('test', 'service', { - 'entity_id': ['group.test_group'] - }) - - extracted = component.extract_from_service(call, expand_group=False) - self.assertEqual([test_group], extracted) - def test_setup_loads_platforms(self): """Test the loading of the platforms.""" component_setup = Mock(return_value=True) @@ -344,13 +135,13 @@ class TestHelpersEntityComponent(unittest.TestCase): assert ('platform_test', {}, {'msg': 'discovery_info'}) == \ mock_setup.call_args[0] - @patch('homeassistant.helpers.entity_component.' + @patch('homeassistant.helpers.entity_platform.' 'async_track_time_interval') def test_set_scan_interval_via_config(self, mock_track): """Test the setting of the scan interval via configuration.""" def platform_setup(hass, config, add_devices, discovery_info=None): """Test the platform setup.""" - add_devices([EntityTest(should_poll=True)]) + add_devices([MockEntity(should_poll=True)]) loader.set_component('test_domain.platform', MockPlatform(platform_setup)) @@ -368,38 +159,13 @@ class TestHelpersEntityComponent(unittest.TestCase): assert mock_track.called assert timedelta(seconds=30) == mock_track.call_args[0][2] - @patch('homeassistant.helpers.entity_component.' - 'async_track_time_interval') - def test_set_scan_interval_via_platform(self, mock_track): - """Test the setting of the scan interval via platform.""" - def platform_setup(hass, config, add_devices, discovery_info=None): - """Test the platform setup.""" - add_devices([EntityTest(should_poll=True)]) - - platform = MockPlatform(platform_setup) - platform.SCAN_INTERVAL = timedelta(seconds=30) - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - - self.hass.block_till_done() - assert mock_track.called - assert timedelta(seconds=30) == mock_track.call_args[0][2] - def test_set_entity_namespace_via_config(self): """Test setting an entity namespace.""" def platform_setup(hass, config, add_devices, discovery_info=None): """Test the platform setup.""" add_devices([ - EntityTest(name='beer'), - EntityTest(name=None), + MockEntity(name='beer'), + MockEntity(name=None), ]) platform = MockPlatform(platform_setup) @@ -420,83 +186,16 @@ class TestHelpersEntityComponent(unittest.TestCase): assert sorted(self.hass.states.entity_ids()) == \ ['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device'] - def test_adding_entities_with_generator_and_thread_callback(self): - """Test generator in add_entities that calls thread method. - - We should make sure we resolve the generator to a list before passing - it into an async context. - """ - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - def create_entity(number): - """Create entity helper.""" - entity = EntityTest() - entity.entity_id = generate_entity_id(component.entity_id_format, - 'Number', hass=self.hass) - return entity - - component.add_entities(create_entity(i) for i in range(2)) - - -@asyncio.coroutine -def test_platform_warn_slow_setup(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - - with patch.object(hass.loop, 'call_later', MagicMock()) \ - as mock_call: - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - assert mock_call.called - - timeout, logger_method = mock_call.mock_calls[0][1][:2] - - assert timeout == SLOW_SETUP_WARNING - assert logger_method == _LOGGER.warning - - assert mock_call().cancel.called - - -@asyncio.coroutine -def test_platform_error_slow_setup(hass, caplog): - """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" - with patch.object(entity_component, 'SLOW_SETUP_MAX_WAIT', 0): - called = [] - - @asyncio.coroutine - def setup_platform(*args): - called.append(1) - yield from asyncio.sleep(1, loop=hass.loop) - - platform = MockPlatform(async_setup_platform=setup_platform) - component = EntityComponent(_LOGGER, DOMAIN, hass) - loader.set_component('test_domain.test_platform', platform) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'test_platform', - } - }) - assert len(called) == 1 - assert 'test_domain.test_platform' not in hass.config.components - assert 'test_platform is taking longer than 0 seconds' in caplog.text - @asyncio.coroutine def test_extract_from_service_available_device(hass): """Test the extraction of entity from service and device is available.""" component = EntityComponent(_LOGGER, DOMAIN, hass) yield from component.async_add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2', available=False), - EntityTest(name='test_3'), - EntityTest(name='test_4', available=False), + MockEntity(name='test_1'), + MockEntity(name='test_2', available=False), + MockEntity(name='test_3'), + MockEntity(name='test_4', available=False), ]) call_1 = ha.ServiceCall('test', 'service') @@ -514,26 +213,6 @@ def test_extract_from_service_available_device(hass): component.async_extract_from_service(call_2)) -@asyncio.coroutine -def test_updated_state_used_for_entity_id(hass): - """Test that first update results used for entity ID generation.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - class EntityTestNameFetcher(EntityTest): - """Mock entity that fetches a friendly name.""" - - @asyncio.coroutine - def async_update(self): - """Mock update that assigns a name.""" - self._values['name'] = "Living Room" - - yield from component.async_add_entities([EntityTestNameFetcher()], True) - - entity_ids = hass.states.async_entity_ids() - assert 1 == len(entity_ids) - assert entity_ids[0] == "test_domain.living_room" - - @asyncio.coroutine def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" @@ -579,108 +258,50 @@ def test_platform_not_ready(hass): @asyncio.coroutine -def test_pararell_updates_async_platform(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - @asyncio.coroutine - def mock_update(*args, **kwargs): - pass - - platform.async_setup_platform = mock_update - - loader.set_component('test_domain.platform', platform) - +def test_extract_from_service_returns_all_if_no_entity_id(hass): + """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} + yield from component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } + call = ha.ServiceCall('test', 'service') + + assert ['test_domain.test_1', 'test_domain.test_2'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call)) + + +@asyncio.coroutine +def test_extract_from_service_filter_out_non_existing_entities(hass): + """Test the extraction of non existing entities from service.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) + + call = ha.ServiceCall('test', 'service', { + 'entity_id': ['test_domain.test_2', 'test_domain.non_exist'] }) - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is None + assert ['test_domain.test_2'] == \ + [ent.entity_id for ent + in component.async_extract_from_service(call)] @asyncio.coroutine -def test_pararell_updates_async_platform_with_constant(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - @asyncio.coroutine - def mock_update(*args, **kwargs): - pass - - platform.async_setup_platform = mock_update - platform.PARALLEL_UPDATES = 1 - - loader.set_component('test_domain.platform', platform) - +def test_extract_from_service_no_group_expand(hass): + """Test not expanding a group.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} + test_group = yield from group.Group.async_create_group( + hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) + yield from component.async_add_entities([test_group]) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } + call = ha.ServiceCall('test', 'service', { + 'entity_id': ['group.test_group'] }) - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is not None - - -@asyncio.coroutine -def test_pararell_updates_sync_platform(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} - - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is not None - - -@asyncio.coroutine -def test_raise_error_on_update(hass): - """Test the add entity if they raise an error on update.""" - updates = [] - component = EntityComponent(_LOGGER, DOMAIN, hass) - entity1 = EntityTest(name='test_1') - entity2 = EntityTest(name='test_2') - - def _raise(): - """Helper to raise a exception.""" - raise AssertionError - - entity1.update = _raise - entity2.update = lambda: updates.append(1) - - yield from component.async_add_entities([entity1, entity2], True) - - assert len(updates) == 1 - assert 1 in updates - - -@asyncio.coroutine -def test_async_remove_with_platform(hass): - """Remove an entity from a platform.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - entity1 = EntityTest(name='test_1') - yield from component.async_add_entities([entity1]) - assert len(hass.states.async_entity_ids()) == 1 - yield from entity1.async_remove() - assert len(hass.states.async_entity_ids()) == 0 + extracted = component.async_extract_from_service(call, expand_group=False) + assert extracted == [test_group] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py new file mode 100644 index 00000000000..4c27cc45a00 --- /dev/null +++ b/tests/helpers/test_entity_platform.py @@ -0,0 +1,435 @@ +"""Tests for the EntityPlatform helper.""" +import asyncio +import logging +import unittest +from unittest.mock import patch, Mock, MagicMock +from datetime import timedelta + +import homeassistant.loader as loader +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity_component import ( + EntityComponent, DEFAULT_SCAN_INTERVAL) +from homeassistant.helpers import entity_platform + +import homeassistant.util.dt as dt_util + +from tests.common import ( + get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, + MockEntity) + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "test_domain" + + +class TestHelpersEntityPlatform(unittest.TestCase): + """Test homeassistant.helpers.entity_component module.""" + + def setUp(self): # pylint: disable=invalid-name + """Initialize a test Home Assistant instance.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Clean up the test Home Assistant instance.""" + self.hass.stop() + + def test_polling_only_updates_entities_it_should_poll(self): + """Test the polling of only updated entities.""" + component = EntityComponent( + _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) + + no_poll_ent = MockEntity(should_poll=False) + no_poll_ent.async_update = Mock() + poll_ent = MockEntity(should_poll=True) + poll_ent.async_update = Mock() + + component.add_entities([no_poll_ent, poll_ent]) + + no_poll_ent.async_update.reset_mock() + poll_ent.async_update.reset_mock() + + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) + self.hass.block_till_done() + + assert not no_poll_ent.async_update.called + assert poll_ent.async_update.called + + def test_polling_updates_entities_with_exception(self): + """Test the updated entities that not break with an exception.""" + component = EntityComponent( + _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) + + update_ok = [] + update_err = [] + + def update_mock(): + """Mock normal update.""" + update_ok.append(None) + + def update_mock_err(): + """Mock error update.""" + update_err.append(None) + raise AssertionError("Fake error update") + + ent1 = MockEntity(should_poll=True) + ent1.update = update_mock_err + ent2 = MockEntity(should_poll=True) + ent2.update = update_mock + ent3 = MockEntity(should_poll=True) + ent3.update = update_mock + ent4 = MockEntity(should_poll=True) + ent4.update = update_mock + + component.add_entities([ent1, ent2, ent3, ent4]) + + update_ok.clear() + update_err.clear() + + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) + self.hass.block_till_done() + + assert len(update_ok) == 3 + assert len(update_err) == 1 + + def test_update_state_adds_entities(self): + """Test if updating poll entities cause an entity to be added works.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent1 = MockEntity() + ent2 = MockEntity(should_poll=True) + + component.add_entities([ent2]) + assert 1 == len(self.hass.states.entity_ids()) + ent2.update = lambda *_: component.add_entities([ent1]) + + fire_time_changed( + self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL + ) + self.hass.block_till_done() + + assert 2 == len(self.hass.states.entity_ids()) + + def test_update_state_adds_entities_with_update_before_add_true(self): + """Test if call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], True) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert ent.update.called + + def test_update_state_adds_entities_with_update_before_add_false(self): + """Test if not call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], False) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert not ent.update.called + + @patch('homeassistant.helpers.entity_platform.' + 'async_track_time_interval') + def test_set_scan_interval_via_platform(self, mock_track): + """Test the setting of the scan interval via platform.""" + def platform_setup(hass, config, add_devices, discovery_info=None): + """Test the platform setup.""" + add_devices([MockEntity(should_poll=True)]) + + platform = MockPlatform(platform_setup) + platform.SCAN_INTERVAL = timedelta(seconds=30) + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + component.setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + self.hass.block_till_done() + assert mock_track.called + assert timedelta(seconds=30) == mock_track.call_args[0][2] + + def test_adding_entities_with_generator_and_thread_callback(self): + """Test generator in add_entities that calls thread method. + + We should make sure we resolve the generator to a list before passing + it into an async context. + """ + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + def create_entity(number): + """Create entity helper.""" + entity = MockEntity() + entity.entity_id = generate_entity_id(DOMAIN + '.{}', + 'Number', hass=self.hass) + return entity + + component.add_entities(create_entity(i) for i in range(2)) + + +@asyncio.coroutine +def test_platform_warn_slow_setup(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + with patch.object(hass.loop, 'call_later', MagicMock()) \ + as mock_call: + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + assert mock_call.called + + timeout, logger_method = mock_call.mock_calls[0][1][:2] + + assert timeout == entity_platform.SLOW_SETUP_WARNING + assert logger_method == _LOGGER.warning + + assert mock_call().cancel.called + + +@asyncio.coroutine +def test_platform_error_slow_setup(hass, caplog): + """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" + with patch.object(entity_platform, 'SLOW_SETUP_MAX_WAIT', 0): + called = [] + + @asyncio.coroutine + def setup_platform(*args): + called.append(1) + yield from asyncio.sleep(1, loop=hass.loop) + + platform = MockPlatform(async_setup_platform=setup_platform) + component = EntityComponent(_LOGGER, DOMAIN, hass) + loader.set_component('test_domain.test_platform', platform) + yield from component.async_setup({ + DOMAIN: { + 'platform': 'test_platform', + } + }) + assert len(called) == 1 + assert 'test_domain.test_platform' not in hass.config.components + assert 'test_platform is taking longer than 0 seconds' in caplog.text + + +@asyncio.coroutine +def test_updated_state_used_for_entity_id(hass): + """Test that first update results used for entity ID generation.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + class MockEntityNameFetcher(MockEntity): + """Mock entity that fetches a friendly name.""" + + @asyncio.coroutine + def async_update(self): + """Mock update that assigns a name.""" + self._values['name'] = "Living Room" + + yield from component.async_add_entities([MockEntityNameFetcher()], True) + + entity_ids = hass.states.async_entity_ids() + assert 1 == len(entity_ids) + assert entity_ids[0] == "test_domain.living_room" + + +@asyncio.coroutine +def test_parallel_updates_async_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is None + + +@asyncio.coroutine +def test_parallel_updates_async_platform_with_constant(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + platform.PARALLEL_UPDATES = 1 + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None + + +@asyncio.coroutine +def test_parallel_updates_sync_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None + + +@asyncio.coroutine +def test_raise_error_on_update(hass): + """Test the add entity if they raise an error on update.""" + updates = [] + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name='test_1') + entity2 = MockEntity(name='test_2') + + def _raise(): + """Helper to raise an exception.""" + raise AssertionError + + entity1.update = _raise + entity2.update = lambda: updates.append(1) + + yield from component.async_add_entities([entity1, entity2], True) + + assert len(updates) == 1 + assert 1 in updates + + +@asyncio.coroutine +def test_async_remove_with_platform(hass): + """Remove an entity from a platform.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name='test_1') + yield from component.async_add_entities([entity1]) + assert len(hass.states.async_entity_ids()) == 1 + yield from entity1.async_remove() + assert len(hass.states.async_entity_ids()) == 0 + + +@asyncio.coroutine +def test_not_adding_duplicate_entities_with_unique_id(hass): + """Test for not adding duplicate entities.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + MockEntity(name='test1', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + yield from component.async_add_entities([ + MockEntity(name='test2', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + +@asyncio.coroutine +def test_using_prescribed_entity_id(hass): + """Test for using predefined entity ID.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(name='bla', entity_id='hello.world')]) + assert 'hello.world' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_with_unique_id(hass): + """Test for ammending predefined entity ID because currently exists.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world')]) + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world', unique_id='bla')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_which_is_registered(hass): + """Test not allowing predefined entity ID that already registered.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + # This entity_id will be rewritten + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_name_which_conflict_with_registered(hass): + """Test not generating conflicting entity ID based on name.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + yield from component.async_add_entities([ + MockEntity(name='world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_entity_with_name_and_entity_id_getting_registered(hass): + """Ensure that entity ID is used for registration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(unique_id='1234', name='bla', + entity_id='test_domain.world')]) + assert 'test_domain.world' in hass.states.async_entity_ids() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py new file mode 100644 index 00000000000..d19a3f3fe49 --- /dev/null +++ b/tests/helpers/test_entity_registry.py @@ -0,0 +1,135 @@ +"""Tests for the Entity Registry.""" +import asyncio +from unittest.mock import patch, mock_open + +import pytest + +from homeassistant.helpers import entity_registry + +from tests.common import mock_registry + + +@pytest.fixture +def registry(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@asyncio.coroutine +def test_get_or_create_returns_same_entry(registry): + """Make sure we do not duplicate entries.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + entry2 = registry.async_get_or_create('light', 'hue', '1234') + + assert len(registry.entities) == 1 + assert entry is entry2 + assert entry.entity_id == 'light.hue_1234' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id(registry): + """Test that suggested_object_id works.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_register(registry): + """Test that we don't generate an entity id that is already registered.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + entry2 = registry.async_get_or_create( + 'light', 'hue', '5678', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + assert entry2.entity_id == 'light.beer_2' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_existing(hass, registry): + """Test that we don't generate an entity id that currently exists.""" + hass.states.async_set('light.hue_1234', 'on') + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234_2' + + +@asyncio.coroutine +def test_create_triggers_save(hass, registry): + """Test that registering entry triggers a save.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + +@asyncio.coroutine +def test_save_timer_reset_on_subsequent_save(hass, registry): + """Test we reset the save timer on a new create.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + with patch.object(hass.loop, 'call_later') as mock_call_later_2: + registry.async_get_or_create('light', 'hue', '5678') + + assert len(mock_call_later().cancel.mock_calls) == 1 + assert len(mock_call_later_2.mock_calls) == 1 + + +@asyncio.coroutine +def test_loading_saving_data(hass, registry): + """Test that we load/save data correctly.""" + yaml_path = 'homeassistant.util.yaml.open' + orig_entry1 = registry.async_get_or_create('light', 'hue', '1234') + orig_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert len(registry.entities) == 2 + + with patch(yaml_path, mock_open(), create=True) as mock_write: + yield from registry._async_save() + + # Mock open calls are: open file, context enter, write, context leave + written = mock_write.mock_calls[2][1][0] + + # Now load written data in new registry + registry2 = entity_registry.EntityRegistry(hass) + + with patch('os.path.isfile', return_value=True), \ + patch(yaml_path, mock_open(read_data=written), create=True): + yield from registry2._async_load() + + # Ensure same order + assert list(registry.entities) == list(registry2.entities) + new_entry1 = registry.async_get_or_create('light', 'hue', '1234') + new_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert orig_entry1 == new_entry1 + assert orig_entry2 == new_entry2 + + +@asyncio.coroutine +def test_generate_entity_considers_registered_entities(registry): + """Test that we don't create entity id that are already registered.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_generate_entity_id('light', 'hue_1234') == \ + 'light.hue_1234_2' + + +@asyncio.coroutine +def test_generate_entity_considers_existing_entities(hass, registry): + """Test that we don't create entity id that currently exists.""" + hass.states.async_set('light.kitchen', 'on') + assert registry.async_generate_entity_id('light', 'kitchen') == \ + 'light.kitchen_2' + + +@asyncio.coroutine +def test_is_registered(registry): + """Test that is_registered works.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert registry.async_is_registered(entry.entity_id) + assert not registry.async_is_registered('light.non_existing') diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 797cd257833..944224a34d1 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -1,4 +1,4 @@ -"""The tests for the EntityFitler component.""" +"""The tests for the EntityFilter component.""" from homeassistant.helpers.entityfilter import generate_filter diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 7d601c7a78d..73f2b9ff5a4 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,10 +7,12 @@ from datetime import datetime, timedelta from astral import Astral import pytest +from homeassistant.core import callback from homeassistant.setup import setup_component import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( + async_call_later, track_point_in_utc_time, track_point_in_time, track_utc_time_change, @@ -52,7 +54,7 @@ class TestEventHelpers(unittest.TestCase): runs = [] track_point_in_utc_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(before_birthday) self.hass.block_till_done() @@ -68,14 +70,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(1, len(runs)) track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(after_birthday) self.hass.block_till_done() self.assertEqual(2, len(runs)) unsub = track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) unsub() self._send_time_changed(after_birthday) @@ -642,3 +644,22 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + + +@asyncio.coroutine +def test_async_call_later(hass): + """Test calling an action later.""" + def action(): pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + remove = async_call_later(hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert hass is hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + assert remove is mock() diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 31d98633ef8..a987f5130f1 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -28,7 +28,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.stop() def test_template_service_call(self): - """Test service call with tempating.""" + """Test service call with templating.""" config = { 'service_template': '{{ \'test_domain.test_service\' }}', 'entity_id': 'hello.world', @@ -68,6 +68,24 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual('goodbye', self.calls[0].data['hello']) + def test_bad_template(self): + """Test passing bad template.""" + config = { + 'service_template': '{{ var_service }}', + 'entity_id': 'hello.world', + 'data_template': { + 'hello': '{{ states + unknown_var }}' + } + } + + service.call_from_config(self.hass, config, variables={ + 'var_service': 'test_domain.test_service', + 'var_data': 'goodbye', + }) + self.hass.block_till_done() + + self.assertEqual(len(self.calls), 0) + def test_split_entity_string(self): """Test splitting of entity string.""" service.call_from_config(self.hass, { @@ -102,7 +120,7 @@ class TestServiceHelpers(unittest.TestCase): @patch('homeassistant.helpers.service._LOGGER.error') def test_fail_silently_if_no_service(self, mock_log): - """Test failling if service is missing.""" + """Test failing if service is missing.""" service.call_from_config(self.hass, None) self.assertEqual(1, mock_log.call_count) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 9047f26b2d1..c109ae30aad 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -17,7 +17,7 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) -# prevent .HA_VERISON file from being written +# prevent .HA_VERSION file from being written @patch( 'homeassistant.bootstrap.conf_util.process_ha_config_upgrade', Mock()) @patch('homeassistant.util.location.detect_location_info', diff --git a/tests/test_config.py b/tests/test_config.py index 377c650e91f..541eaf4f79e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -255,18 +255,6 @@ class TestConfig(unittest.TestCase): return self.hass.states.get('test.test') - def test_entity_customization_false(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: { - 'test.test': {'hidden': False}}} - - state = self._compute_state(config) - - assert 'hidden' not in state.attributes - def test_entity_customization(self): """Test entity customization through configuration.""" config = {CONF_LATITUDE: 50, diff --git a/tests/test_core.py b/tests/test_core.py index ea952a7c073..77a7872526f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -135,7 +135,7 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(3): + for _ in range(3): self.hass.add_job(test_coro()) run_coroutine_threadsafe( @@ -155,7 +155,7 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(2): + for _ in range(2): self.hass.add_job(test_coro()) @asyncio.coroutine @@ -172,7 +172,7 @@ class TestHomeAssistant(unittest.TestCase): assert len(call_count) == 2 def test_async_add_job_pending_tasks_executor(self): - """Run a executor in pending tasks.""" + """Run an executor in pending tasks.""" call_count = [] def test_executor(): @@ -185,7 +185,7 @@ class TestHomeAssistant(unittest.TestCase): yield from asyncio.sleep(0, loop=self.hass.loop) yield from asyncio.sleep(0, loop=self.hass.loop) - for i in range(2): + for _ in range(2): self.hass.add_job(test_executor) run_coroutine_threadsafe( @@ -210,7 +210,7 @@ class TestHomeAssistant(unittest.TestCase): yield from asyncio.sleep(0, loop=self.hass.loop) yield from asyncio.sleep(0, loop=self.hass.loop) - for i in range(2): + for _ in range(2): self.hass.add_job(test_callback) run_coroutine_threadsafe( diff --git a/tests/test_remote.py b/tests/test_remote.py index 41011794914..9aa730d6eb6 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -28,7 +28,7 @@ def _url(path=''): # pylint: disable=invalid-name def setUpModule(): - """Initalization of a Home Assistant server instance.""" + """Initialization of a Home Assistant server instance.""" global hass, master_api hass = get_test_home_assistant() diff --git a/tests/test_requirements.py b/tests/test_requirements.py new file mode 100644 index 00000000000..946e64af847 --- /dev/null +++ b/tests/test_requirements.py @@ -0,0 +1,61 @@ +"""Test requirements module.""" +import os +from unittest import mock + +from homeassistant import loader, setup +from homeassistant.requirements import CONSTRAINT_FILE + +from tests.common import get_test_home_assistant, MockModule + + +class TestRequirements: + """Test the requirements module.""" + + hass = None + backup_cache = None + + # pylint: disable=invalid-name, no-self-use + def setup_method(self, method): + """Setup the test.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Clean up.""" + self.hass.stop() + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=True) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_venv( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in virtual environment.""" + mock_venv.return_value = True + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=False) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_deps( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in deps directory.""" + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', target=self.hass.config.path('deps'), + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) diff --git a/tests/test_setup.py b/tests/test_setup.py index afea30ddcd1..6a94310793c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONSTRAINT_FILE +from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util @@ -41,9 +41,6 @@ class TestSetup: """Clean up.""" self.hass.stop() - # if os.path.isfile(VERSION_PATH): - # os.remove(VERSION_PATH) - def test_validate_component_config(self): """Test validating component configuration.""" config_schema = vol.Schema({ @@ -203,43 +200,6 @@ class TestSetup: assert not setup.setup_component(self.hass, 'comp') assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=True) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in virtual environment.""" - mock_venv.return_value = True - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=False) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in deps directory.""" - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', target=self.hass.config.path('deps'), - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - def test_component_not_setup_twice_if_loaded_during_other_setup(self): """Test component setup while waiting for lock is not setup twice.""" result = [] diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d11a71d541f..d7033775a14 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -97,7 +97,7 @@ class AiohttpClientMockResponse: """Mock Aiohttp client response.""" def __init__(self, method, url, status, response, cookies=None, exc=None, - headers={}): + headers=None): """Initialize a fake response.""" self.method = method self._url = url @@ -107,7 +107,7 @@ class AiohttpClientMockResponse: self.response = response self.exc = exc - self._headers = headers + self._headers = headers or {} self._cookies = {} if cookies: diff --git a/tests/util/test_async.py b/tests/util/test_async.py index b7a18d00fae..b6ae58a484f 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,7 +1,7 @@ """Tests for async util methods from Python source.""" import asyncio -from asyncio import test_utils from unittest.mock import MagicMock, patch +from unittest import TestCase import pytest @@ -104,14 +104,32 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): assert len(loop.call_soon_threadsafe.mock_calls) == 2 -class RunThreadsafeTests(test_utils.TestCase): - """Test case for asyncio.run_coroutine_threadsafe.""" +class RunThreadsafeTests(TestCase): + """Test case for hasync.run_coroutine_threadsafe.""" def setUp(self): """Test setup method.""" - super().setUp() self.loop = asyncio.new_event_loop() - self.set_event_loop(self.loop) # Will cleanup properly + + def tearDown(self): + """Test teardown method.""" + executor = self.loop._default_executor + if executor is not None: + executor.shutdown(wait=True) + self.loop.close() + + @staticmethod + def run_briefly(loop): + """Momentarily run a coroutine on the given loop.""" + @asyncio.coroutine + def once(): + pass + gen = once() + t = loop.create_task(gen) + try: + loop.run_until_complete(t) + finally: + gen.close() def add_callback(self, a, b, fail, invalid): """Return a + b.""" @@ -185,7 +203,7 @@ class RunThreadsafeTests(test_utils.TestCase): future = self.loop.run_in_executor(None, callback) with self.assertRaises(asyncio.TimeoutError): self.loop.run_until_complete(future) - test_utils.run_briefly(self.loop) + self.run_briefly(self.loop) # Check that there's no pending task (add has been cancelled) for task in asyncio.Task.all_tasks(self.loop): self.assertTrue(task.done()) diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 7f04f6f0569..2ad3b42fdb8 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -1,4 +1,4 @@ -"""Test homeasssitant distance utility functions.""" +"""Test homeassistant distance utility functions.""" import unittest import homeassistant.util.distance as distance_util diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 38b957ad102..734f4b548b9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -48,7 +48,7 @@ class TestYaml(unittest.TestCase): load_yaml_config_file(YAML_CONFIG_FILE) def test_no_key(self): - """Test item without an key.""" + """Test item without a key.""" files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'} with self.assertRaises(HomeAssistantError), \ patch_yaml_files(files):