diff --git a/.coveragerc b/.coveragerc index b5939bafec6..ab4e91e5efd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -742,6 +742,7 @@ omit = homeassistant/components/venstar/climate.py homeassistant/components/vera/* homeassistant/components/verisure/* + homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 0daf10d0566..c9d9dc129a5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 +homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py new file mode 100644 index 00000000000..4f378f4ab00 --- /dev/null +++ b/homeassistant/components/versasense/__init__.py @@ -0,0 +1,97 @@ +"""Support for VersaSense MicroPnP devices.""" +import logging + +import pyversasense as pyv +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + PERIPHERAL_CLASS_SENSOR, + PERIPHERAL_CLASS_SENSOR_ACTUATOR, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "versasense" + +# Validation of the user's configuration +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass, config): + """Set up the versasense component.""" + session = aiohttp_client.async_get_clientsession(hass) + consumer = pyv.Consumer(config[DOMAIN]["host"], session) + + hass.data[DOMAIN] = {KEY_CONSUMER: consumer} + + await _configure_entities(hass, config, consumer) + + # Return boolean to indicate that initialization was successful. + return True + + +async def _configure_entities(hass, config, consumer): + """Fetch all devices with their peripherals for representation.""" + devices = await consumer.fetchDevices() + _LOGGER.debug(devices) + + sensor_info_list = [] + switch_info_list = [] + + for mac, device in devices.items(): + _LOGGER.info("Device connected: %s %s", device.name, mac) + hass.data[DOMAIN][mac] = {} + + for peripheral_id, peripheral in device.peripherals.items(): + hass.data[DOMAIN][mac][peripheral_id] = peripheral + + if peripheral.classification == PERIPHERAL_CLASS_SENSOR: + sensor_info_list = _add_entity_info_to_list( + peripheral, device, sensor_info_list + ) + elif peripheral.classification == PERIPHERAL_CLASS_SENSOR_ACTUATOR: + switch_info_list = _add_entity_info_to_list( + peripheral, device, switch_info_list + ) + + if sensor_info_list: + _load_platform(hass, config, "sensor", sensor_info_list) + + if switch_info_list: + _load_platform(hass, config, "switch", switch_info_list) + + +def _add_entity_info_to_list(peripheral, device, entity_info_list): + """Add info from a peripheral to specified list.""" + for measurement in peripheral.measurements: + entity_info = { + KEY_IDENTIFIER: peripheral.identifier, + KEY_UNIT: measurement.unit, + KEY_MEASUREMENT: measurement.name, + KEY_PARENT_NAME: device.name, + KEY_PARENT_MAC: device.mac, + } + + entity_info_list.append(entity_info) + + return entity_info_list + + +def _load_platform(hass, config, entity_type, entity_info_list): + """Load platform with list of entity info.""" + hass.async_create_task( + async_load_platform(hass, entity_type, DOMAIN, entity_info_list, config) + ) diff --git a/homeassistant/components/versasense/const.py b/homeassistant/components/versasense/const.py new file mode 100644 index 00000000000..5283f61ac26 --- /dev/null +++ b/homeassistant/components/versasense/const.py @@ -0,0 +1,11 @@ +"""Constants for versasense.""" +KEY_CONSUMER = "consumer" +KEY_IDENTIFIER = "identifier" +KEY_MEASUREMENT = "measurement" +KEY_PARENT_MAC = "parent_mac" +KEY_PARENT_NAME = "parent_name" +KEY_UNIT = "unit" +PERIPHERAL_CLASS_SENSOR = "sensor" +PERIPHERAL_CLASS_SENSOR_ACTUATOR = "sensor-actuator" +PERIPHERAL_STATE_OFF = "OFF" +PERIPHERAL_STATE_ON = "ON" diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json new file mode 100644 index 00000000000..3e2be6131d1 --- /dev/null +++ b/homeassistant/components/versasense/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "versasense", + "name": "VersaSense", + "documentation": "https://www.home-assistant.io/components/versasense", + "dependencies": [], + "codeowners": ["@flamm3blemuff1n"], + "requirements": ["pyversasense==0.0.6"] +} diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py new file mode 100644 index 00000000000..4253bfcbba4 --- /dev/null +++ b/homeassistant/components/versasense/sensor.py @@ -0,0 +1,97 @@ +"""Support for VersaSense sensor peripheral.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN +from .const import ( + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + sensor_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + sensor_list.append( + VSensor(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(sensor_list) + + +class VSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._state = None + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def available(self): + """Return if the sensor is available.""" + return self._available + + async def async_update(self): + """Fetch new state data for the sensor.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + self._state = sample.value + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._state = None diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py new file mode 100644 index 00000000000..4ea118a6c97 --- /dev/null +++ b/homeassistant/components/versasense/switch.py @@ -0,0 +1,113 @@ +"""Support for VersaSense actuator peripheral.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN +from .const import ( + PERIPHERAL_STATE_ON, + PERIPHERAL_STATE_OFF, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up actuator platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + actuator_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + actuator_list.append( + VActuator(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(actuator_list) + + +class VActuator(SwitchDevice): + """Representation of an Actuator.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._is_on = False + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the actuator.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the actuator.""" + return self._name + + @property + def is_on(self): + """Return the state of the actuator.""" + return self._is_on + + @property + def available(self): + """Return if the actuator is available.""" + return self._available + + async def async_turn_off(self, **kwargs): + """Turn off the actuator.""" + await self.update_state(0) + + async def async_turn_on(self, **kwargs): + """Turn on the actuator.""" + await self.update_state(1) + + async def update_state(self, state): + """Update the state of the actuator.""" + payload = {"id": "state-num", "value": state} + + await self.consumer.actuatePeripheral( + None, self._identifier, self._parent_mac, payload + ) + + async def async_update(self): + """Fetch state data from the actuator.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + if sample.value == PERIPHERAL_STATE_OFF: + self._is_on = False + elif sample.value == PERIPHERAL_STATE_ON: + self._is_on = True + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._is_on = None diff --git a/requirements_all.txt b/requirements_all.txt index 77db871edc1..b368f35349b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1655,6 +1655,9 @@ pyuptimerobot==0.0.5 # homeassistant.components.vera pyvera==0.3.6 +# homeassistant.components.versasense +pyversasense==0.0.6 + # homeassistant.components.vesync pyvesync==1.1.0