Skip to content

Commit

Permalink
Mobile App: Sensors (#21854)
Browse files Browse the repository at this point in the history
## Description:

**Related issue (if applicable):** fixes #21782

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
  - [x] There is no commented out code in this PR.
  • Loading branch information
robbiet480 authored Mar 15, 2019
1 parent 6a80ffa commit dcaced1
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 30 deletions.
36 changes: 22 additions & 14 deletions homeassistant/components/mobile_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.components.webhook import async_register as webhook_register
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.typing import ConfigType, HomeAssistantType

from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, DATA_BINARY_SENSOR,
DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES,
DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION)
DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY,
STORAGE_VERSION)

from .http_api import RegistrationsView
from .webhook import handle_webhook
Expand All @@ -22,19 +22,25 @@

async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the mobile app component."""
hass.data[DOMAIN] = {
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
}

store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
app_config = await store.async_load()
if app_config is None:
app_config = {
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
DATA_BINARY_SENSOR: {},
DATA_CONFIG_ENTRIES: {},
DATA_DELETED_IDS: [],
DATA_DEVICES: {},
DATA_SENSOR: {}
}

hass.data[DOMAIN] = app_config
hass.data[DOMAIN][DATA_STORE] = store
hass.data[DOMAIN] = {
DATA_BINARY_SENSOR: app_config.get(DATA_BINARY_SENSOR, {}),
DATA_CONFIG_ENTRIES: {},
DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []),
DATA_DEVICES: {},
DATA_SENSOR: app_config.get(DATA_SENSOR, {}),
DATA_STORE: store,
}

hass.http.register_view(RegistrationsView())
register_websocket_handlers(hass)
Expand Down Expand Up @@ -79,9 +85,11 @@ async def async_setup_entry(hass, entry):
webhook_register(hass, DOMAIN, registration_name, webhook_id,
handle_webhook)

if ATTR_APP_COMPONENT in registration:
load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {},
{DOMAIN: {}})
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry,
DATA_BINARY_SENSOR))
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR))

return True

Expand Down
54 changes: 54 additions & 0 deletions homeassistant/components/mobile_app/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Binary sensor platform for mobile_app."""
from functools import partial

from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from .const import (ATTR_SENSOR_STATE,
ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE,
DATA_DEVICES, DOMAIN)

from .entity import MobileAppEntity

DEPENDENCIES = ['mobile_app']


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up mobile app binary sensor from a config entry."""
entities = list()

webhook_id = config_entry.data[CONF_WEBHOOK_ID]

for config in hass.data[DOMAIN][ENTITY_TYPE].values():
if config[CONF_WEBHOOK_ID] != webhook_id:
continue

device = hass.data[DOMAIN][DATA_DEVICES][webhook_id]

entities.append(MobileAppBinarySensor(config, device, config_entry))

async_add_entities(entities)

@callback
def handle_sensor_registration(webhook_id, data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return

device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]

async_add_entities([MobileAppBinarySensor(data, device, config_entry)])

async_dispatcher_connect(hass,
'{}_{}_register'.format(DOMAIN, ENTITY_TYPE),
partial(handle_sensor_registration, webhook_id))


class MobileAppBinarySensor(MobileAppEntity, BinarySensorDevice):
"""Representation of an mobile app binary sensor."""

@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._config[ATTR_SENSOR_STATE]
56 changes: 53 additions & 3 deletions homeassistant/components/mobile_app/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Constants for mobile_app."""
import voluptuous as vol

from homeassistant.components.binary_sensor import (DEVICE_CLASSES as
BINARY_SENSOR_CLASSES)
from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES
from homeassistant.components.device_tracker import (ATTR_BATTERY,
ATTR_GPS,
ATTR_GPS_ACCURACY,
Expand All @@ -17,9 +20,11 @@
CONF_SECRET = 'secret'
CONF_USER_ID = 'user_id'

DATA_BINARY_SENSOR = 'binary_sensor'
DATA_CONFIG_ENTRIES = 'config_entries'
DATA_DELETED_IDS = 'deleted_ids'
DATA_DEVICES = 'devices'
DATA_SENSOR = 'sensor'
DATA_STORE = 'store'

ATTR_APP_COMPONENT = 'app_component'
Expand Down Expand Up @@ -54,16 +59,22 @@

ERR_ENCRYPTION_REQUIRED = 'encryption_required'
ERR_INVALID_COMPONENT = 'invalid_component'
ERR_SENSOR_NOT_REGISTERED = 'not_registered'
ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id'

WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor'
WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template'
WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location'
WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration'
WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states'

WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT,
WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION]
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION,
WEBHOOK_TYPE_UPDATE_SENSOR_STATES]


REGISTRATION_SCHEMA = vol.Schema({
Expand Down Expand Up @@ -91,7 +102,7 @@

WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({
vol.Required(ATTR_WEBHOOK_TYPE): cv.string, # vol.In(WEBHOOK_TYPES)
vol.Required(ATTR_WEBHOOK_DATA, default={}): dict,
vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list),
vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean,
vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string,
})
Expand Down Expand Up @@ -125,10 +136,49 @@
vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int,
})

ATTR_SENSOR_ATTRIBUTES = 'attributes'
ATTR_SENSOR_DEVICE_CLASS = 'device_class'
ATTR_SENSOR_ICON = 'icon'
ATTR_SENSOR_NAME = 'name'
ATTR_SENSOR_STATE = 'state'
ATTR_SENSOR_TYPE = 'type'
ATTR_SENSOR_TYPE_BINARY_SENSOR = 'binary_sensor'
ATTR_SENSOR_TYPE_SENSOR = 'sensor'
ATTR_SENSOR_UNIQUE_ID = 'unique_id'
ATTR_SENSOR_UOM = 'unit_of_measurement'

SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR]

COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES))

SIGNAL_SENSOR_UPDATE = DOMAIN + '_sensor_update'

REGISTER_SENSOR_SCHEMA = vol.Schema({
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All(vol.Lower,
vol.In(COMBINED_CLASSES)),
vol.Required(ATTR_SENSOR_NAME): cv.string,
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
vol.Required(ATTR_SENSOR_UOM): cv.string,
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float),
vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon,
})

UPDATE_SENSOR_STATE_SCHEMA = vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon,
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float),
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
})])

WEBHOOK_SCHEMAS = {
WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA,
WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA,
WEBHOOK_TYPE_REGISTER_SENSOR: REGISTER_SENSOR_SCHEMA,
WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA,
WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA,
WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA,
WEBHOOK_TYPE_UPDATE_SENSOR_STATES: UPDATE_SENSOR_STATE_SCHEMA,
}
98 changes: 98 additions & 0 deletions homeassistant/components/mobile_app/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""A entity class for mobile_app."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity

from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES,
ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON,
ATTR_SENSOR_NAME, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID,
DOMAIN, SIGNAL_SENSOR_UPDATE)


class MobileAppEntity(Entity):
"""Representation of an mobile app entity."""

def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry):
"""Initialize the sensor."""
self._config = config
self._device = device
self._entry = entry
self._registration = entry.data
self._sensor_id = "{}_{}".format(self._registration[CONF_WEBHOOK_ID],
config[ATTR_SENSOR_UNIQUE_ID])
self._entity_type = config[ATTR_SENSOR_TYPE]
self.unsub_dispatcher = None

async def async_added_to_hass(self):
"""Register callbacks."""
self.unsub_dispatcher = async_dispatcher_connect(self.hass,
SIGNAL_SENSOR_UPDATE,
self._handle_update)

async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()

@property
def should_poll(self) -> bool:
"""Declare that this entity pushes its state to HA."""
return False

@property
def name(self):
"""Return the name of the mobile app sensor."""
return self._config[ATTR_SENSOR_NAME]

@property
def device_class(self):
"""Return the device class."""
return self._config.get(ATTR_SENSOR_DEVICE_CLASS)

@property
def device_state_attributes(self):
"""Return the device state attributes."""
return self._config[ATTR_SENSOR_ATTRIBUTES]

@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._config[ATTR_SENSOR_ICON]

@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return self._sensor_id

@property
def device_info(self):
"""Return device registry information for this entity."""
return {
'identifiers': {
(ATTR_DEVICE_ID, self._registration[ATTR_DEVICE_ID]),
(CONF_WEBHOOK_ID, self._registration[CONF_WEBHOOK_ID])
},
'manufacturer': self._registration[ATTR_MANUFACTURER],
'model': self._registration[ATTR_MODEL],
'device_name': self._registration[ATTR_DEVICE_NAME],
'sw_version': self._registration[ATTR_OS_VERSION],
'config_entries': self._device.config_entries
}

async def async_update(self):
"""Get the latest state of the sensor."""
data = self.hass.data[DOMAIN]
try:
self._config = data[self._entity_type][self._sensor_id]
except KeyError:
return

@callback
def _handle_update(self, data):
"""Handle async event updates."""
self._config = data
self.async_schedule_update_ha_state()
5 changes: 4 additions & 1 deletion homeassistant/components/mobile_app/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN)
CONF_SECRET, CONF_USER_ID, DATA_BINARY_SENSOR,
DATA_DELETED_IDS, DATA_SENSOR, DOMAIN)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -123,7 +124,9 @@ def safe_registration(registration: Dict) -> Dict:
def savable_state(hass: HomeAssistantType) -> Dict:
"""Return a clean object containing things that should be saved."""
return {
DATA_BINARY_SENSOR: hass.data[DOMAIN][DATA_BINARY_SENSOR],
DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
DATA_SENSOR: hass.data[DOMAIN][DATA_SENSOR],
}


Expand Down
Loading

0 comments on commit dcaced1

Please sign in to comment.