From 1a7e8c88a3666a469488b08c1c9984684151b9d2 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 7 Jun 2018 23:30:20 +0300 Subject: [PATCH] Wireless tags platform (#13495) * Initial version of wirelesstags platform support. * Pinned wirelesstagpy, generated requirements_all. * Fixed temperature units in imperial units systems, make binary events more tags specific. * Lowercased tag name during entity_id generation. * Fixed: tag_id template for tag_binary_events, support of light sensor for homebridge. * Minor style cleanup. * Removed state, define_name, icon. Reworked async arm/disarm update. Removed static attrs. Introduced available property. Custom events contains components name now. Cleaned dedundant items from schema definition. * Removed comment and beep duration from attributes. Minor cleanup of documentation comment. * Ignoring Wemo switches linked in iOS app. * Reworked passing data from platform to components using signals. --- .coveragerc | 3 + .../components/binary_sensor/wirelesstag.py | 214 +++++++++++++++ .../components/sensor/wirelesstag.py | 176 ++++++++++++ .../components/switch/wirelesstag.py | 118 ++++++++ homeassistant/components/wirelesstag.py | 256 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 770 insertions(+) create mode 100644 homeassistant/components/binary_sensor/wirelesstag.py create mode 100755 homeassistant/components/sensor/wirelesstag.py create mode 100644 homeassistant/components/switch/wirelesstag.py create mode 100644 homeassistant/components/wirelesstag.py diff --git a/.coveragerc b/.coveragerc index e38ceeba5ce..a7d222b33b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -311,6 +311,9 @@ omit = homeassistant/components/wink/* homeassistant/components/*/wink.py + homeassistant/components/wirelesstag.py + homeassistant/components/*/wirelesstag.py + homeassistant/components/xiaomi_aqara.py homeassistant/components/*/xiaomi_aqara.py diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py new file mode 100644 index 00000000000..bfc2d44fc6e --- /dev/null +++ b/homeassistant/components/binary_sensor/wirelesstag.py @@ -0,0 +1,214 @@ +""" +Binary sensor support for Wireless Sensor Tags. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.wirelesstag/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + SIGNAL_BINARY_EVENT_UPDATE, + WirelessTagBaseSensor) +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +# On means in range, Off means out of range +SENSOR_PRESENCE = 'presence' + +# On means motion detected, Off means cear +SENSOR_MOTION = 'motion' + +# On means open, Off means closed +SENSOR_DOOR = 'door' + +# On means temperature become too cold, Off means normal +SENSOR_COLD = 'cold' + +# On means hot, Off means normal +SENSOR_HEAT = 'heat' + +# On means too dry (humidity), Off means normal +SENSOR_DRY = 'dry' + +# On means too wet (humidity), Off means normal +SENSOR_WET = 'wet' + +# On means light detected, Off means no light +SENSOR_LIGHT = 'light' + +# On means moisture detected (wet), Off means no moisture (dry) +SENSOR_MOISTURE = 'moisture' + +# On means tag battery is low, Off means normal +SENSOR_BATTERY = 'low_battery' + +# Sensor types: Name, device_class, push notification type representing 'on', +# attr to check +SENSOR_TYPES = { + SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', { + "on": "oor", + "off": "back_in_range" + }, 2], + SENSOR_MOTION: ['Motion', 'motion', 'is_moved', { + "on": "motion_detected", + }, 5], + SENSOR_DOOR: ['Door', 'door', 'is_door_open', { + "on": "door_opened", + "off": "door_closed" + }, 5], + SENSOR_COLD: ['Cold', 'cold', 'is_cold', { + "on": "temp_toolow", + "off": "temp_normal" + }, 4], + SENSOR_HEAT: ['Heat', 'heat', 'is_heat', { + "on": "temp_toohigh", + "off": "temp_normal" + }, 4], + SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', { + "on": "too_dry", + "off": "cap_normal" + }, 2], + SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', { + "on": "too_humid", + "off": "cap_normal" + }, 2], + SENSOR_LIGHT: ['Light', 'light', 'is_light_on', { + "on": "too_bright", + "off": "light_normal" + }, 1], + SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', { + "on": "water_detected", + "off": "water_dried", + }, 1], + SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', { + "on": "low_battery" + }, 3] +} + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the platform for a WirelessTags.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + + sensors = [] + tags = platform.tags + for tag in tags.values(): + allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag) + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in allowed_sensor_types: + sensors.append(WirelessTagBinarySensor(platform, tag, + sensor_type)) + + add_devices(sensors, True) + hass.add_job(platform.install_push_notifications, sensors) + + +class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): + """A binary sensor implementation for WirelessTags.""" + + @classmethod + def allowed_sensors(cls, tag): + """Return list of allowed sensor types for specific tag type.""" + sensors_map = { + # 13-bit tag - allows everything but not light and moisture + WIRELESSTAG_TYPE_13BIT: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_MOTION, SENSOR_DOOR, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_DRY, SENSOR_WET], + + # Moister/water sensor - temperature and moisture only + WIRELESSTAG_TYPE_WATER: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_MOISTURE], + + # ALS Pro: allows everything, but not moisture + WIRELESSTAG_TYPE_ALSPRO: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_MOTION, SENSOR_DOOR, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_DRY, SENSOR_WET, + SENSOR_LIGHT], + + # Wemo are power switches. + WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE] + } + + # allow everything if tag type is unknown + # (i just dont have full catalog of them :)) + tag_type = tag.tag_type + fullset = SENSOR_TYPES.keys() + return sensors_map[tag_type] if tag_type in sensors_map else fullset + + def __init__(self, api, tag, sensor_type): + """Initialize a binary sensor for a Wireless Sensor Tags.""" + super().__init__(api, tag) + self._sensor_type = sensor_type + self._name = '{0} {1}'.format(self._tag.name, + SENSOR_TYPES[self._sensor_type][0]) + self._device_class = SENSOR_TYPES[self._sensor_type][1] + self._tag_attr = SENSOR_TYPES[self._sensor_type][2] + self.binary_spec = SENSOR_TYPES[self._sensor_type][3] + self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4] + + async def async_added_to_hass(self): + """Register callbacks.""" + tag_id = self.tag_id + event_type = self.device_class + async_dispatcher_connect( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), + self._on_binary_event_callback) + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._state == STATE_ON + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def principal_value(self): + """Return value of tag. + + Subclasses need override based on type of sensor. + """ + return ( + STATE_ON if getattr(self._tag, self._tag_attr, False) + else STATE_OFF) + + def updated_state_value(self): + """Use raw princial value.""" + return self.principal_value + + @callback + def _on_binary_event_callback(self, event): + """Update state from arrive push notification.""" + # state should be 'on' or 'off' + self._state = event.data.get('state') + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py new file mode 100755 index 00000000000..c93da3c791f --- /dev/null +++ b/homeassistant/components/sensor/wirelesstag.py @@ -0,0 +1,176 @@ +""" +Sensor support for Wirelss Sensor Tags platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.wirelesstag/ +""" + +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS) +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + SIGNAL_TAG_UPDATE, + WirelessTagBaseSensor) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TEMPERATURE = 'temperature' +SENSOR_HUMIDITY = 'humidity' +SENSOR_MOISTURE = 'moisture' +SENSOR_LIGHT = 'light' + +SENSOR_TYPES = { + SENSOR_TEMPERATURE: { + 'unit': TEMP_CELSIUS, + 'attr': 'temperature' + }, + SENSOR_HUMIDITY: { + 'unit': '%', + 'attr': 'humidity' + }, + SENSOR_MOISTURE: { + 'unit': '%', + 'attr': 'moisture' + }, + SENSOR_LIGHT: { + 'unit': 'lux', + 'attr': 'light' + } +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + sensors = [] + tags = platform.tags + for tag in tags.values(): + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in WirelessTagSensor.allowed_sensors(tag): + sensors.append(WirelessTagSensor( + platform, tag, sensor_type, hass.config)) + + add_devices(sensors, True) + + +class WirelessTagSensor(WirelessTagBaseSensor): + """Representation of a Sensor.""" + + @classmethod + def allowed_sensors(cls, tag): + """Return array of allowed sensor types for tag.""" + all_sensors = SENSOR_TYPES.keys() + sensors_per_tag_type = { + WIRELESSTAG_TYPE_13BIT: [ + SENSOR_TEMPERATURE, + SENSOR_HUMIDITY], + WIRELESSTAG_TYPE_WATER: [ + SENSOR_TEMPERATURE, + SENSOR_MOISTURE], + WIRELESSTAG_TYPE_ALSPRO: [ + SENSOR_TEMPERATURE, + SENSOR_HUMIDITY, + SENSOR_LIGHT], + WIRELESSTAG_TYPE_WEMO_DEVICE: [] + } + + tag_type = tag.tag_type + return ( + sensors_per_tag_type[tag_type] if tag_type in sensors_per_tag_type + else all_sensors) + + def __init__(self, api, tag, sensor_type, config): + """Constructor with platform(api), tag and hass sensor type.""" + super().__init__(api, tag) + + self._sensor_type = sensor_type + self._tag_attr = SENSOR_TYPES[self._sensor_type]['attr'] + self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit'] + self._name = self._tag.name + + # I want to see entity_id as: + # sensor.wirelesstag_bedroom_temperature + # and not as sensor.bedroom for temperature and + # sensor.bedroom_2 for humidity + self._entity_id = '{}.{}_{}_{}'.format('sensor', WIRELESSTAG_DOMAIN, + self.underscored_name, + self._sensor_type) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, + SIGNAL_TAG_UPDATE.format(self.tag_id), + self._update_tag_info_callback) + + @property + def entity_id(self): + """Overriden version.""" + return self._entity_id + + @property + def underscored_name(self): + """Provide name savvy to be used in entity_id name of self.""" + return self.name.lower().replace(" ", "_") + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._sensor_type + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def principal_value(self): + """Return sensor current value.""" + return getattr(self._tag, self._tag_attr, False) + + @callback + def _update_tag_info_callback(self, event): + """Handle push notification sent by tag manager.""" + if event.data.get('id') != self.tag_id: + return + + _LOGGER.info("Entity to update state: %s event data: %s", + self, event.data) + new_value = self.principal_value + try: + if self._sensor_type == SENSOR_TEMPERATURE: + new_value = event.data.get('temp') + elif (self._sensor_type == SENSOR_HUMIDITY or + self._sensor_type == SENSOR_MOISTURE): + new_value = event.data.get('cap') + elif self._sensor_type == SENSOR_LIGHT: + new_value = event.data.get('lux') + except Exception as error: # pylint: disable=W0703 + _LOGGER.info("Unable to update value of entity: \ + %s error: %s event: %s", self, error, event) + + self._state = self.decorate_value(new_value) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/wirelesstag.py b/homeassistant/components/switch/wirelesstag.py new file mode 100644 index 00000000000..cce8c349a31 --- /dev/null +++ b/homeassistant/components/switch/wirelesstag.py @@ -0,0 +1,118 @@ +""" +Switch implementation for Wireless Sensor Tags (wirelesstag.net) platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.wirelesstag/ +""" +import logging + +import voluptuous as vol + + +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + WirelessTagBaseSensor) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +ARM_TEMPERATURE = 'temperature' +ARM_HUMIDITY = 'humidity' +ARM_MOTION = 'motion' +ARM_LIGHT = 'light' +ARM_MOISTURE = 'moisture' + +# Switch types: Name, tag sensor type +SWITCH_TYPES = { + ARM_TEMPERATURE: ['Arm Temperature', 'temperature'], + ARM_HUMIDITY: ['Arm Humidity', 'humidity'], + ARM_MOTION: ['Arm Motion', 'motion'], + ARM_LIGHT: ['Arm Light', 'light'], + ARM_MOISTURE: ['Arm Moisture', 'moisture'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up switches for a Wireless Sensor Tags.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + + switches = [] + tags = platform.load_tags() + for switch_type in config.get(CONF_MONITORED_CONDITIONS): + for _, tag in tags.items(): + if switch_type in WirelessTagSwitch.allowed_switches(tag): + switches.append(WirelessTagSwitch(platform, tag, switch_type)) + + add_devices(switches, True) + + +class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice): + """A switch implementation for Wireless Sensor Tags.""" + + @classmethod + def allowed_switches(cls, tag): + """Return allowed switch types for wireless tag.""" + all_sensors = SWITCH_TYPES.keys() + sensors_per_tag_spec = { + WIRELESSTAG_TYPE_13BIT: [ + ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION], + WIRELESSTAG_TYPE_WATER: [ + ARM_TEMPERATURE, ARM_MOISTURE], + WIRELESSTAG_TYPE_ALSPRO: [ + ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION, ARM_LIGHT], + WIRELESSTAG_TYPE_WEMO_DEVICE: [] + } + + tag_type = tag.tag_type + + result = ( + sensors_per_tag_spec[tag_type] + if tag_type in sensors_per_tag_spec else all_sensors) + _LOGGER.info("Allowed switches: %s tag_type: %s", + str(result), tag_type) + + return result + + def __init__(self, api, tag, switch_type): + """Initialize a switch for Wireless Sensor Tag.""" + super().__init__(api, tag) + self._switch_type = switch_type + self.sensor_type = SWITCH_TYPES[self._switch_type][1] + self._name = '{} {}'.format(self._tag.name, + SWITCH_TYPES[self._switch_type][0]) + + def turn_on(self, **kwargs): + """Turn on the switch.""" + self._api.arm(self) + + def turn_off(self, **kwargs): + """Turn on the switch.""" + self._api.disarm(self) + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._state + + def updated_state_value(self): + """Provide formatted value.""" + return self.principal_value + + @property + def principal_value(self): + """Provide actual value of switch.""" + attr_name = 'is_{}_sensor_armed'.format(self.sensor_type) + return getattr(self._tag, attr_name, False) diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py new file mode 100644 index 00000000000..9fabcb1cd5a --- /dev/null +++ b/homeassistant/components/wirelesstag.py @@ -0,0 +1,256 @@ +""" +Wireless Sensor Tags platform support. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/wirelesstag/ +""" +import logging + +from requests.exceptions import HTTPError, ConnectTimeout +import voluptuous as vol +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + dispatcher_send) + +REQUIREMENTS = ['wirelesstagpy==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + + +# straight of signal in dBm +ATTR_TAG_SIGNAL_STRAIGHT = 'signal_straight' +# indicates if tag is out of range or not +ATTR_TAG_OUT_OF_RANGE = 'out_of_range' +# number in percents from max power of tag receiver +ATTR_TAG_POWER_CONSUMPTION = 'power_consumption' + + +NOTIFICATION_ID = 'wirelesstag_notification' +NOTIFICATION_TITLE = "Wireless Sensor Tag Setup" + +DOMAIN = 'wirelesstag' +DEFAULT_ENTITY_NAMESPACE = 'wirelesstag' + +WIRELESSTAG_TYPE_13BIT = 13 +WIRELESSTAG_TYPE_ALSPRO = 26 +WIRELESSTAG_TYPE_WATER = 32 +WIRELESSTAG_TYPE_WEMO_DEVICE = 82 + +SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}' +SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +class WirelessTagPlatform: + """Principal object to manage all registered in HA tags.""" + + def __init__(self, hass, api): + """Designated initializer for wirelesstags platform.""" + self.hass = hass + self.api = api + self.tags = {} + + def load_tags(self): + """Load tags from remote server.""" + self.tags = self.api.load_tags() + return self.tags + + def arm(self, switch): + """Arm entity sensor monitoring.""" + func_name = 'arm_{}'.format(switch.sensor_type) + arm_func = getattr(self.api, func_name) + if arm_func is not None: + arm_func(switch.tag_id) + + def disarm(self, switch): + """Disarm entity sensor monitoring.""" + func_name = 'disarm_{}'.format(switch.sensor_type) + disarm_func = getattr(self.api, func_name) + if disarm_func is not None: + disarm_func(switch.tag_id) + + # pylint: disable=no-self-use + def make_push_notitication(self, name, url, content): + """Factory for notification config.""" + from wirelesstagpy import NotificationConfig + return NotificationConfig(name, { + 'url': url, 'verb': 'POST', + 'content': content, 'disabled': False, 'nat': True}) + + def install_push_notifications(self, binary_sensors): + """Setup local push notification from tag manager.""" + _LOGGER.info("Registering local push notifications.") + configs = [] + + binary_url = self.binary_event_callback_url + for event in binary_sensors: + for state, name in event.binary_spec.items(): + content = ('{"type": "' + event.device_class + + '", "id":{' + str(event.tag_id_index_template) + + '}, "state": \"' + state + '\"}') + config = self.make_push_notitication(name, binary_url, content) + configs.append(config) + + content = ("{\"name\":\"{0}\",\"id\":{1},\"temp\":{2}," + + "\"cap\":{3},\"lux\":{4}}") + update_url = self.update_callback_url + update_config = self.make_push_notitication( + 'update', update_url, content) + configs.append(update_config) + + result = self.api.install_push_notification(0, configs, True) + if not result: + self.hass.components.persistent_notification.create( + "Error: failed to install local push notifications
", + title="Wireless Sensor Tag Setup Local Push Notifications", + notification_id="wirelesstag_failed_push_notification") + else: + _LOGGER.info("Installed push notifications for all tags.") + + @property + def update_callback_url(self): + """Return url for local push notifications(update event).""" + return '{}/api/events/wirelesstag_update_tags'.format( + self.hass.config.api.base_url) + + @property + def binary_event_callback_url(self): + """Return url for local push notifications(binary event).""" + return '{}/api/events/wirelesstag_binary_event'.format( + self.hass.config.api.base_url) + + def handle_update_tags_event(self, event): + """Main entry to handle push event from wireless tag manager.""" + _LOGGER.info("push notification for update arrived: %s", event) + dispatcher_send( + self.hass, + SIGNAL_TAG_UPDATE.format(event.data.get('id')), + event) + + def handle_binary_event(self, event): + """Handle push notifications for binary (on/off) events.""" + _LOGGER.info("Push notification for binary event arrived: %s", event) + try: + tag_id = event.data.get('id') + event_type = event.data.get('type') + dispatcher_send( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), + event) + except Exception as ex: # pylint: disable=W0703 + _LOGGER.error("Unable to handle binary event:\ + %s error: %s", str(event), str(ex)) + + +def setup(hass, config): + """Set up the Wireless Sensor Tag component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + try: + from wirelesstagpy import (WirelessTags, WirelessTagsException) + wirelesstags = WirelessTags(username=username, password=password) + + platform = WirelessTagPlatform(hass, wirelesstags) + platform.load_tags() + hass.data[DOMAIN] = platform + except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: + _LOGGER.error("Unable to connect to wirelesstag.net service: %s", + str(ex)) + hass.components.persistent_notification.create( + "Error: {}
" + "Please restart hass after fixing this." + "".format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + # listen to custom events + hass.bus.listen('wirelesstag_update_tags', + hass.data[DOMAIN].handle_update_tags_event) + hass.bus.listen('wirelesstag_binary_event', + hass.data[DOMAIN].handle_binary_event) + + return True + + +class WirelessTagBaseSensor(Entity): + """Base class for HA implementation for Wireless Sensor Tag.""" + + def __init__(self, api, tag): + """Initialize a base sensor for Wireless Sensor Tag platform.""" + self._api = api + self._tag = tag + self._uuid = self._tag.uuid + self.tag_id = self._tag.tag_id + self._name = self._tag.name + self._state = None + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def principal_value(self): + """Return base value. + + Subclasses need override based on type of sensor. + """ + return 0 + + def updated_state_value(self): + """Default implementation formats princial value.""" + return self.decorate_value(self.principal_value) + + # pylint: disable=no-self-use + def decorate_value(self, value): + """Decorate input value to be well presented for end user.""" + return '{:.1f}'.format(value) + + @property + def available(self): + """Return True if entity is available.""" + return self._tag.is_alive + + def update(self): + """Update state.""" + if not self.should_poll: + return + + updated_tags = self._api.load_tags() + updated_tag = updated_tags[self._uuid] + if updated_tag is None: + _LOGGER.error('Unable to update tag: "%s"', self.name) + return + + self._tag = updated_tag + self._state = self.updated_state_value() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_BATTERY_LEVEL: self._tag.battery_remaining, + ATTR_VOLTAGE: '{:.2f}V'.format(self._tag.battery_volts), + ATTR_TAG_SIGNAL_STRAIGHT: '{}dBm'.format( + self._tag.signal_straight), + ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, + ATTR_TAG_POWER_CONSUMPTION: '{:.2f}%'.format( + self._tag.power_consumption) + } diff --git a/requirements_all.txt b/requirements_all.txt index d2b52d17961..6bbe2a0b79e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,6 +1378,9 @@ websocket-client==0.37.0 # homeassistant.components.media_player.webostv websockets==3.2 +# homeassistant.components.wirelesstag +wirelesstagpy==0.3.0 + # homeassistant.components.zigbee xbee-helper==0.0.7