Mobile App: Sensors (#21854)
## 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.pull/22059/head
parent
6a80ffa8cc
commit
dcaced1966
|
@ -3,13 +3,13 @@ from homeassistant import config_entries
|
|||
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
|
||||
|
@ -22,19 +22,25 @@ REQUIREMENTS = ['PyNaCl==1.3.0']
|
|||
|
||||
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)
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
|
@ -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,
|
||||
|
@ -17,9 +20,11 @@ CONF_CLOUDHOOK_URL = 'cloudhook_url'
|
|||
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'
|
||||
|
@ -54,16 +59,22 @@ ATTR_WEBHOOK_TYPE = 'type'
|
|||
|
||||
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({
|
||||
|
@ -91,7 +102,7 @@ UPDATE_REGISTRATION_SCHEMA = vol.Schema({
|
|||
|
||||
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,
|
||||
})
|
||||
|
@ -125,10 +136,49 @@ UPDATE_LOCATION_SCHEMA = vol.Schema({
|
|||
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,
|
||||
}
|
||||
|
|
|
@ -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()
|
|
@ -11,7 +11,8 @@ from homeassistant.helpers.typing import HomeAssistantType
|
|||
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__)
|
||||
|
||||
|
@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
"""Sensor platform for mobile_app."""
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (ATTR_SENSOR_STATE,
|
||||
ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE,
|
||||
ATTR_SENSOR_UOM, 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 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(MobileAppSensor(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([MobileAppSensor(data, device, config_entry)])
|
||||
|
||||
async_dispatcher_connect(hass,
|
||||
'{}_{}_register'.format(DOMAIN, ENTITY_TYPE),
|
||||
partial(handle_sensor_registration, webhook_id))
|
||||
|
||||
|
||||
class MobileAppSensor(MobileAppEntity):
|
||||
"""Representation of an mobile app sensor."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._config[ATTR_SENSOR_STATE]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement this sensor expresses itself in."""
|
||||
return self._config[ATTR_SENSOR_UOM]
|
|
@ -10,30 +10,38 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
|
|||
SERVICE_SEE as DT_SEE)
|
||||
|
||||
from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
|
||||
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST)
|
||||
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED)
|
||||
from homeassistant.core import EventOrigin
|
||||
from homeassistant.exceptions import (ServiceNotFound, TemplateError)
|
||||
from homeassistant.exceptions import (HomeAssistantError,
|
||||
ServiceNotFound, TemplateError)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.template import attach
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
|
||||
ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME,
|
||||
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED,
|
||||
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
|
||||
ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SPEED,
|
||||
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
|
||||
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
|
||||
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
|
||||
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
|
||||
CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DOMAIN,
|
||||
ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA,
|
||||
CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS,
|
||||
DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED,
|
||||
ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED,
|
||||
SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA,
|
||||
WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE,
|
||||
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE,
|
||||
WEBHOOK_TYPE_UPDATE_LOCATION,
|
||||
WEBHOOK_TYPE_UPDATE_REGISTRATION)
|
||||
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_REGISTER_SENSOR,
|
||||
WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION,
|
||||
WEBHOOK_TYPE_UPDATE_REGISTRATION,
|
||||
WEBHOOK_TYPE_UPDATE_SENSOR_STATES)
|
||||
|
||||
|
||||
from .helpers import (_decrypt_payload, empty_okay_response, error_response,
|
||||
registration_context, safe_registration,
|
||||
registration_context, safe_registration, savable_state,
|
||||
webhook_response)
|
||||
|
||||
|
||||
|
@ -79,6 +87,10 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
|||
enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA]
|
||||
webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data)
|
||||
|
||||
if webhook_type not in WEBHOOK_SCHEMAS:
|
||||
_LOGGER.error('Received invalid webhook type: %s', webhook_type)
|
||||
return empty_okay_response()
|
||||
|
||||
try:
|
||||
data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload)
|
||||
except vol.Invalid as ex:
|
||||
|
@ -172,3 +184,80 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
|||
|
||||
return webhook_response(safe_registration(new_registration),
|
||||
registration=registration, headers=headers)
|
||||
|
||||
if webhook_type == WEBHOOK_TYPE_REGISTER_SENSOR:
|
||||
entity_type = data[ATTR_SENSOR_TYPE]
|
||||
|
||||
unique_id = data[ATTR_SENSOR_UNIQUE_ID]
|
||||
|
||||
unique_store_key = "{}_{}".format(webhook_id, unique_id)
|
||||
|
||||
if unique_store_key in hass.data[DOMAIN][entity_type]:
|
||||
_LOGGER.error("Refusing to re-register existing sensor %s!",
|
||||
unique_id)
|
||||
return error_response(ERR_SENSOR_DUPLICATE_UNIQUE_ID,
|
||||
"{} {} already exists!".format(entity_type,
|
||||
unique_id),
|
||||
status=409)
|
||||
|
||||
data[CONF_WEBHOOK_ID] = webhook_id
|
||||
|
||||
hass.data[DOMAIN][entity_type][unique_store_key] = data
|
||||
|
||||
try:
|
||||
await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass))
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error("Error registering sensor: %s", ex)
|
||||
return empty_okay_response()
|
||||
|
||||
register_signal = '{}_{}_register'.format(DOMAIN,
|
||||
data[ATTR_SENSOR_TYPE])
|
||||
async_dispatcher_send(hass, register_signal, data)
|
||||
|
||||
return webhook_response({"status": "registered"},
|
||||
registration=registration, status=HTTP_CREATED,
|
||||
headers=headers)
|
||||
|
||||
if webhook_type == WEBHOOK_TYPE_UPDATE_SENSOR_STATES:
|
||||
resp = {}
|
||||
for sensor in data:
|
||||
entity_type = sensor[ATTR_SENSOR_TYPE]
|
||||
|
||||
unique_id = sensor[ATTR_SENSOR_UNIQUE_ID]
|
||||
|
||||
unique_store_key = "{}_{}".format(webhook_id, unique_id)
|
||||
|
||||
if unique_store_key not in hass.data[DOMAIN][entity_type]:
|
||||
_LOGGER.error("Refusing to update non-registered sensor: %s",
|
||||
unique_store_key)
|
||||
err_msg = '{} {} is not registered'.format(entity_type,
|
||||
unique_id)
|
||||
resp[unique_id] = {
|
||||
'success': False,
|
||||
'error': {
|
||||
'code': ERR_SENSOR_NOT_REGISTERED,
|
||||
'message': err_msg
|
||||
}
|
||||
}
|
||||
continue
|
||||
|
||||
entry = hass.data[DOMAIN][entity_type][unique_store_key]
|
||||
|
||||
new_state = {**entry, **sensor}
|
||||
|
||||
hass.data[DOMAIN][entity_type][unique_store_key] = new_state
|
||||
|
||||
safe = savable_state(hass)
|
||||
|
||||
try:
|
||||
await hass.data[DOMAIN][DATA_STORE].async_save(safe)
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error("Error updating mobile_app registration: %s", ex)
|
||||
return empty_okay_response()
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
|
||||
|
||||
resp[unique_id] = {"status": "okay"}
|
||||
|
||||
return webhook_response(resp, registration=registration,
|
||||
headers=headers)
|
||||
|
|
|
@ -6,9 +6,9 @@ from tests.common import mock_device_registry
|
|||
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from homeassistant.components.mobile_app.const import (DATA_CONFIG_ENTRIES,
|
||||
from homeassistant.components.mobile_app.const import (DATA_BINARY_SENSOR,
|
||||
DATA_DELETED_IDS,
|
||||
DATA_DEVICES,
|
||||
DATA_SENSOR,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION)
|
||||
|
@ -48,7 +48,9 @@ async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user):
|
|||
hass_storage[STORAGE_KEY] = {
|
||||
'version': STORAGE_VERSION,
|
||||
'data': {
|
||||
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
|
||||
DATA_BINARY_SENSOR: {},
|
||||
DATA_DELETED_IDS: [],
|
||||
DATA_SENSOR: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
"""Entity tests for mobile_app."""
|
||||
# pylint: disable=redefined-outer-name,unused-import
|
||||
import logging
|
||||
|
||||
from . import (authed_api_client, create_registrations, # noqa: F401
|
||||
webhook_client) # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401, F811, E501
|
||||
"""Test that sensors can be registered and updated."""
|
||||
webhook_id = create_registrations[1]['webhook_id']
|
||||
webhook_url = '/api/webhook/{}'.format(webhook_id)
|
||||
|
||||
reg_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
'type': 'register_sensor',
|
||||
'data': {
|
||||
'attributes': {
|
||||
'foo': 'bar'
|
||||
},
|
||||
'device_class': 'battery',
|
||||
'icon': 'mdi:battery',
|
||||
'name': 'Battery State',
|
||||
'state': 100,
|
||||
'type': 'sensor',
|
||||
'unique_id': 'battery_state',
|
||||
'unit_of_measurement': '%'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert reg_resp.status == 201
|
||||
|
||||
json = await reg_resp.json()
|
||||
assert json == {'status': 'registered'}
|
||||
|
||||
# 3 because we require device_tracker which adds zone.home and
|
||||
# group.all_devices
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
entity = hass.states.async_all()[2]
|
||||
|
||||
assert entity.attributes['device_class'] == 'battery'
|
||||
assert entity.attributes['icon'] == 'mdi:battery'
|
||||
assert entity.attributes['unit_of_measurement'] == '%'
|
||||
assert entity.attributes['foo'] == 'bar'
|
||||
assert entity.domain == 'sensor'
|
||||
assert entity.name == 'Battery State'
|
||||
assert entity.state == '100'
|
||||
|
||||
update_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
'type': 'update_sensor_states',
|
||||
'data': [
|
||||
{
|
||||
'icon': 'mdi:battery-unknown',
|
||||
'state': 123,
|
||||
'type': 'sensor',
|
||||
'unique_id': 'battery_state'
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert update_resp.status == 200
|
||||
|
||||
updated_entity = hass.states.async_all()[2]
|
||||
|
||||
assert updated_entity.state == '123'
|
||||
|
||||
|
||||
async def test_sensor_must_register(hass, create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F401, F811, E501
|
||||
"""Test that sensors must be registered before updating."""
|
||||
webhook_id = create_registrations[1]['webhook_id']
|
||||
webhook_url = '/api/webhook/{}'.format(webhook_id)
|
||||
resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
'type': 'update_sensor_states',
|
||||
'data': [
|
||||
{
|
||||
'state': 123,
|
||||
'type': 'sensor',
|
||||
'unique_id': 'battery_state'
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
|
||||
json = await resp.json()
|
||||
assert json['battery_state']['success'] is False
|
||||
assert json['battery_state']['error']['code'] == 'not_registered'
|
||||
|
||||
|
||||
async def test_sensor_id_no_dupes(hass, create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F401, F811, E501
|
||||
"""Test that sensors must have a unique ID."""
|
||||
webhook_id = create_registrations[1]['webhook_id']
|
||||
webhook_url = '/api/webhook/{}'.format(webhook_id)
|
||||
|
||||
payload = {
|
||||
'type': 'register_sensor',
|
||||
'data': {
|
||||
'attributes': {
|
||||
'foo': 'bar'
|
||||
},
|
||||
'device_class': 'battery',
|
||||
'icon': 'mdi:battery',
|
||||
'name': 'Battery State',
|
||||
'state': 100,
|
||||
'type': 'sensor',
|
||||
'unique_id': 'battery_state',
|
||||
'unit_of_measurement': '%'
|
||||
}
|
||||
}
|
||||
|
||||
reg_resp = await webhook_client.post(webhook_url, json=payload)
|
||||
|
||||
assert reg_resp.status == 201
|
||||
|
||||
reg_json = await reg_resp.json()
|
||||
assert reg_json == {'status': 'registered'}
|
||||
|
||||
dupe_resp = await webhook_client.post(webhook_url, json=payload)
|
||||
|
||||
assert dupe_resp.status == 409
|
||||
|
||||
dupe_json = await dupe_resp.json()
|
||||
assert dupe_json['success'] is False
|
||||
assert dupe_json['error']['code'] == 'duplicate_unique_id'
|
Loading…
Reference in New Issue