From bf41674e066d8494942fd1adaebe6112b7aae81b Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 26 Feb 2018 08:01:01 +0000 Subject: [PATCH] Adds simulated sensor (#12539) * Create simulated.py * Create test_simulated.py * Update .coveragerc * Drop numpy and fix attributes Drop numpy and fix attributes to be machine readble * Update test_simulated.py * Update simulated.py * Update test_simulated.py * Update simulated.py * Update test_simulated.py * Update simulated.py * Update simulated.py * Update test_simulated.py * Update simulated.py * Fix default random seed error * Update simulated.py * Addresses balloob comments * Update simulated.py --- .coveragerc | 1 + homeassistant/components/sensor/simulated.py | 146 +++++++++++++++++++ tests/components/sensor/test_simulated.py | 50 +++++++ 3 files changed, 197 insertions(+) create mode 100644 homeassistant/components/sensor/simulated.py create mode 100644 tests/components/sensor/test_simulated.py diff --git a/.coveragerc b/.coveragerc index f073f72cf24..46981341bac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -624,6 +624,7 @@ omit = homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/shodan.py + homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py new file mode 100644 index 00000000000..297f2db9fc0 --- /dev/null +++ b/homeassistant/components/sensor/simulated.py @@ -0,0 +1,146 @@ +""" +Adds a simulated sensor. + +For more details about this platform, refer to the documentation at +https://home-assistant.io/components/sensor.simulated/ +""" +import asyncio +import datetime as datetime +import math +from random import Random +import logging + +import voluptuous as vol + +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_NAME +from homeassistant.components.sensor import PLATFORM_SCHEMA + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = datetime.timedelta(seconds=1) +ICON = 'mdi:chart-line' + +CONF_UNIT = 'unit' +CONF_AMP = 'amplitude' +CONF_MEAN = 'mean' +CONF_PERIOD = 'period' +CONF_PHASE = 'phase' +CONF_FWHM = 'spread' +CONF_SEED = 'seed' + +DEFAULT_NAME = 'simulated' +DEFAULT_UNIT = 'value' +DEFAULT_AMP = 1 +DEFAULT_MEAN = 0 +DEFAULT_PERIOD = 60 +DEFAULT_PHASE = 0 +DEFAULT_FWHM = 0 +DEFAULT_SEED = 999 + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, + vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), + vol.Optional(CONF_MEAN, default=DEFAULT_MEAN): vol.Coerce(float), + vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.positive_int, + vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float), + vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), + vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the simulated sensor.""" + name = config.get(CONF_NAME) + unit = config.get(CONF_UNIT) + amp = config.get(CONF_AMP) + mean = config.get(CONF_MEAN) + period = config.get(CONF_PERIOD) + phase = config.get(CONF_PHASE) + fwhm = config.get(CONF_FWHM) + seed = config.get(CONF_SEED) + + sensor = SimulatedSensor( + name, unit, amp, mean, period, phase, fwhm, seed + ) + add_devices([sensor], True) + + +class SimulatedSensor(Entity): + """Class for simulated sensor.""" + + def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed): + """Init the class.""" + self._name = name + self._unit = unit + self._amp = amp + self._mean = mean + self._period = period + self._phase = phase # phase in degrees + self._fwhm = fwhm + self._seed = seed + self._random = Random(seed) # A local seeded Random + self._start_time = dt_util.utcnow() + self._state = None + + def time_delta(self): + """"Return the time delta.""" + dt0 = self._start_time + dt1 = dt_util.utcnow() + return dt1 - dt0 + + def signal_calc(self): + """Calculate the signal.""" + mean = self._mean + amp = self._amp + time_delta = self.time_delta().total_seconds()*1e6 # to milliseconds + period = self._period*1e6 # to milliseconds + fwhm = self._fwhm/2 + phase = math.radians(self._phase) + if period == 0: + periodic = 0 + else: + periodic = amp * (math.sin((2*math.pi*time_delta/period) + phase)) + noise = self._random.gauss(mu=0, sigma=fwhm) + return mean + periodic + noise + + @asyncio.coroutine + def async_update(self): + """Update the sensor.""" + self._state = self.signal_calc() + + @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 icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attr = { + 'amplitude': self._amp, + 'mean': self._mean, + 'period': self._period, + 'phase': self._phase, + 'spread': self._fwhm, + 'seed': self._seed, + } + return attr diff --git a/tests/components/sensor/test_simulated.py b/tests/components/sensor/test_simulated.py new file mode 100644 index 00000000000..3bfccc629fd --- /dev/null +++ b/tests/components/sensor/test_simulated.py @@ -0,0 +1,50 @@ +"""The tests for the simulated sensor.""" +import unittest + +from homeassistant.components.sensor.simulated import ( + CONF_UNIT, CONF_AMP, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_FWHM, + CONF_SEED, DEFAULT_NAME, DEFAULT_AMP, DEFAULT_MEAN, + DEFAULT_PHASE, DEFAULT_FWHM, DEFAULT_SEED) +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + + +class TestSimulatedSensor(unittest.TestCase): + """Test the simulated sensor.""" + + def setup_method(self, method): + """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_default_config(self): + """Test default config.""" + config = { + 'sensor': { + 'platform': 'simulated'} + } + self.assertTrue( + setup_component(self.hass, 'sensor', config)) + self.hass.block_till_done() + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('sensor.simulated') + assert state.attributes.get( + CONF_FRIENDLY_NAME) == DEFAULT_NAME + assert state.attributes.get( + CONF_AMP) == DEFAULT_AMP + assert state.attributes.get( + CONF_UNIT) is None + assert state.attributes.get( + CONF_MEAN) == DEFAULT_MEAN + assert state.attributes.get( + CONF_PERIOD) == 60.0 + assert state.attributes.get( + CONF_PHASE) == DEFAULT_PHASE + assert state.attributes.get( + CONF_FWHM) == DEFAULT_FWHM + assert state.attributes.get( + CONF_SEED) == DEFAULT_SEED