From 2f2952e0ec04dc1dc85c5d295f77cde26cabbb23 Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Sun, 25 Jun 2017 22:48:05 +0200 Subject: [PATCH] Openhardwaremonitor (#8056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Open Hardware Monitor sensor Platform which is able to connect to the JSON API of Open Hardware Monitor and adds sensors for the devices. * Remove copyright in header, not needed. * - Removed old code - Fixed typo’s in comments - Removed log spamming - Removed code that was unnecessary - Use requests instead of urllib - Moved sensor update functionality to data handler, to remove unwanted constructor parameters * Fixed typo in comment Added tests * Added default fixture, to stabilize tests * - Fix for values deeper than 4 levels, no longer relies on fixed level - Fixed tests * Removed timer in preference of helper methods * Moved update functionality back to Entity…. Updated SCAN INTERVAL * Added timeout to request Removed retry when Open Hardware Monitor API is not reachable Fixed naming of sensors Flow optimalisations Fixed tests to use states * Remove unused import --- .../components/sensor/openhardwaremonitor.py | 183 ++++++ .../sensor/test_openhardwaremonitor.py | 40 ++ tests/fixtures/openhardwaremonitor.json | 571 ++++++++++++++++++ 3 files changed, 794 insertions(+) create mode 100644 homeassistant/components/sensor/openhardwaremonitor.py create mode 100644 tests/components/sensor/test_openhardwaremonitor.py create mode 100644 tests/fixtures/openhardwaremonitor.json diff --git a/homeassistant/components/sensor/openhardwaremonitor.py b/homeassistant/components/sensor/openhardwaremonitor.py new file mode 100644 index 00000000000..1d805916d97 --- /dev/null +++ b/homeassistant/components/sensor/openhardwaremonitor.py @@ -0,0 +1,183 @@ +"""Support for Open Hardware Monitor Sensor Platform.""" + +from datetime import timedelta +import logging +import requests +import voluptuous as vol + +from homeassistant.util.dt import utcnow +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +STATE_MIN_VALUE = 'minimal_value' +STATE_MAX_VALUE = 'maximum_value' +STATE_VALUE = 'value' +STATE_OBJECT = 'object' +CONF_INTERVAL = 'interval' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) +SCAN_INTERVAL = timedelta(seconds=30) +RETRY_INTERVAL = timedelta(seconds=30) + +OHM_VALUE = 'Value' +OHM_MIN = 'Min' +OHM_MAX = 'Max' +OHM_CHILDREN = 'Children' +OHM_NAME = 'Text' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=8085): cv.port +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Open Hardware Monitor platform.""" + data = OpenHardwareMonitorData(config, hass) + add_devices(data.devices, True) + + +class OpenHardwareMonitorDevice(Entity): + """Device used to display information from OpenHardwareMonitor.""" + + def __init__(self, data, name, path, unit_of_measurement): + """Initialize an OpenHardwareMonitor sensor.""" + self._name = name + self._data = data + self.path = path + self.attributes = {} + self._unit_of_measurement = unit_of_measurement + + self.value = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the device.""" + return self.value + + @property + def state_attributes(self): + """Return the state attributes of the sun.""" + return self.attributes + + def update(self): + """Update the device from a new JSON object.""" + self._data.update() + + array = self._data.data[OHM_CHILDREN] + _attributes = {} + + for path_index in range(0, len(self.path)): + path_number = self.path[path_index] + values = array[path_number] + + if path_index == len(self.path) - 1: + self.value = values[OHM_VALUE].split(' ')[0] + _attributes.update({ + 'name': values[OHM_NAME], + STATE_MIN_VALUE: values[OHM_MIN].split(' ')[0], + STATE_MAX_VALUE: values[OHM_MAX].split(' ')[0] + }) + + self.attributes = _attributes + return + else: + array = array[path_number][OHM_CHILDREN] + _attributes.update({ + 'level_%s' % path_index: values[OHM_NAME] + }) + + +class OpenHardwareMonitorData(object): + """Class used to pull data from OHM and create sensors.""" + + def __init__(self, config, hass): + """Initialize the Open Hardware Monitor data-handler.""" + self.data = None + self._config = config + self._hass = hass + self.devices = [] + self.initialize(utcnow()) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Hit by the timer with the configured interval.""" + if self.data is None: + self.initialize(utcnow()) + else: + self.refresh() + + def refresh(self): + """Download and parse JSON from OHM.""" + data_url = "http://%s:%d/data.json" % ( + self._config.get(CONF_HOST), + self._config.get(CONF_PORT)) + + try: + response = requests.get(data_url, timeout=30) + self.data = response.json() + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is OpenHardwareMonitor running?") + + def initialize(self, now): + """Initial parsing of the sensors and adding of devices.""" + self.refresh() + + if self.data is None: + return + + self.devices = self.parse_children(self.data, [], [], []) + + def parse_children(self, json, devices, path, names): + """Recursively loop through child objects, finding the values.""" + result = devices.copy() + + if len(json[OHM_CHILDREN]) > 0: + for child_index in range(0, len(json[OHM_CHILDREN])): + child_path = path.copy() + child_path.append(child_index) + + child_names = names.copy() + if len(path) > 0: + child_names.append(json[OHM_NAME]) + + obj = json[OHM_CHILDREN][child_index] + + added_devices = self.parse_children( + obj, devices, child_path, child_names) + + result = result + added_devices + return result + + if json[OHM_VALUE].find(' ') == -1: + return result + + unit_of_measurement = json[OHM_VALUE].split(' ')[1] + child_names = names.copy() + child_names.append(json[OHM_NAME]) + fullname = ' '.join(child_names) + + dev = OpenHardwareMonitorDevice( + self, + fullname, + path, + unit_of_measurement + ) + + result.append(dev) + return result diff --git a/tests/components/sensor/test_openhardwaremonitor.py b/tests/components/sensor/test_openhardwaremonitor.py new file mode 100644 index 00000000000..f66b6dcb3b5 --- /dev/null +++ b/tests/components/sensor/test_openhardwaremonitor.py @@ -0,0 +1,40 @@ +"""The tests for the Open Hardware Monitor platform.""" +import unittest +import requests_mock +from homeassistant.setup import setup_component +from tests.common import load_fixture, get_test_home_assistant + + +class TestOpenHardwareMonitorSetup(unittest.TestCase): + """Test the Open Hardware Monitor platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.config = { + 'sensor': { + 'platform': 'openhardwaremonitor', + 'host': 'localhost', + 'port': 8085 + } + } + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker() + def test_setup(self, mock_req): + """Test for successfully setting up the platform.""" + mock_req.get('http://localhost:8085/data.json', + text=load_fixture('openhardwaremonitor.json')) + + self.assertTrue(setup_component(self.hass, 'sensor', self.config)) + entities = self.hass.states.async_entity_ids('sensor') + self.assertEqual(len(entities), 38) + + state = self.hass.states.get( + 'sensor.testpc_intel_core_i77700_clocks_bus_speed') + + self.assertIsNot(state, None) + self.assertEqual(state.state, '100') diff --git a/tests/fixtures/openhardwaremonitor.json b/tests/fixtures/openhardwaremonitor.json new file mode 100644 index 00000000000..13c5b5481e0 --- /dev/null +++ b/tests/fixtures/openhardwaremonitor.json @@ -0,0 +1,571 @@ +{ + "id": 0, + "Text": "Sensor", + "Children": [ + { + "id": 1, + "Text": "TEST-PC", + "Children": [ + { + "id": 2, + "Text": "ASUS PRIME Z270-P", + "Children": [], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/mainboard.png" + }, + { + "id": 3, + "Text": "Intel Core i7-7700", + "Children": [ + { + "id": 4, + "Text": "Clocks", + "Children": [ + { + "id": 5, + "Text": "Bus Speed", + "Children": [], + "Min": "100 MHz", + "Value": "100 MHz", + "Max": "100 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 6, + "Text": "CPU Core #1", + "Children": [], + "Min": "800 MHz", + "Value": "800 MHz", + "Max": "4200 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 7, + "Text": "CPU Core #2", + "Children": [], + "Min": "800 MHz", + "Value": "800 MHz", + "Max": "4200 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 8, + "Text": "CPU Core #3", + "Children": [], + "Min": "800 MHz", + "Value": "800 MHz", + "Max": "4200 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 9, + "Text": "CPU Core #4", + "Children": [], + "Min": "800 MHz", + "Value": "800 MHz", + "Max": "4200 MHz", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/clock.png" + }, + { + "id": 10, + "Text": "Temperatures", + "Children": [ + { + "id": 11, + "Text": "CPU Core #1", + "Children": [], + "Min": "29.0 °C", + "Value": "31.0 °C", + "Max": "60.0 °C", + "ImageURL": "images/transparent.png" + }, + { + "id": 12, + "Text": "CPU Core #2", + "Children": [], + "Min": "29.0 °C", + "Value": "30.0 °C", + "Max": "61.0 °C", + "ImageURL": "images/transparent.png" + }, + { + "id": 13, + "Text": "CPU Core #3", + "Children": [], + "Min": "28.0 °C", + "Value": "29.0 °C", + "Max": "58.0 °C", + "ImageURL": "images/transparent.png" + }, + { + "id": 14, + "Text": "CPU Core #4", + "Children": [], + "Min": "29.0 °C", + "Value": "31.0 °C", + "Max": "57.0 °C", + "ImageURL": "images/transparent.png" + }, + { + "id": 15, + "Text": "CPU Package", + "Children": [], + "Min": "30.0 °C", + "Value": "31.0 °C", + "Max": "61.0 °C", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png" + }, + { + "id": 16, + "Text": "Load", + "Children": [ + { + "id": 17, + "Text": "CPU Total", + "Children": [], + "Min": "0.0 %", + "Value": "1.0 %", + "Max": "42.2 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 18, + "Text": "CPU Core #1", + "Children": [], + "Min": "0.0 %", + "Value": "1.6 %", + "Max": "50.8 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 19, + "Text": "CPU Core #2", + "Children": [], + "Min": "0.0 %", + "Value": "1.6 %", + "Max": "52.0 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 20, + "Text": "CPU Core #3", + "Children": [], + "Min": "0.0 %", + "Value": "0.0 %", + "Max": "52.2 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 21, + "Text": "CPU Core #4", + "Children": [], + "Min": "0.0 %", + "Value": "0.8 %", + "Max": "51.8 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png" + }, + { + "id": 22, + "Text": "Powers", + "Children": [ + { + "id": 23, + "Text": "CPU Package", + "Children": [], + "Min": "4.4 W", + "Value": "12.1 W", + "Max": "44.6 W", + "ImageURL": "images/transparent.png" + }, + { + "id": 24, + "Text": "CPU Cores", + "Children": [], + "Min": "0.9 W", + "Value": "1.0 W", + "Max": "33.5 W", + "ImageURL": "images/transparent.png" + }, + { + "id": 25, + "Text": "CPU Graphics", + "Children": [], + "Min": "0.0 W", + "Value": "0.0 W", + "Max": "0.0 W", + "ImageURL": "images/transparent.png" + }, + { + "id": 26, + "Text": "CPU DRAM", + "Children": [], + "Min": "1.0 W", + "Value": "1.0 W", + "Max": "2.4 W", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/power.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/cpu.png" + }, + { + "id": 27, + "Text": "Generic Memory", + "Children": [ + { + "id": 28, + "Text": "Load", + "Children": [ + { + "id": 29, + "Text": "Memory", + "Children": [], + "Min": "13.1 %", + "Value": "13.6 %", + "Max": "14.5 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png" + }, + { + "id": 30, + "Text": "Data", + "Children": [ + { + "id": 31, + "Text": "Used Memory", + "Children": [], + "Min": "4.2 GB", + "Value": "4.3 GB", + "Max": "4.6 GB", + "ImageURL": "images/transparent.png" + }, + { + "id": 32, + "Text": "Available Memory", + "Children": [], + "Min": "27.2 GB", + "Value": "27.5 GB", + "Max": "27.7 GB", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/power.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/ram.png" + }, + { + "id": 33, + "Text": "NVIDIA GeForce GTX 1080", + "Children": [ + { + "id": 34, + "Text": "Clocks", + "Children": [ + { + "id": 35, + "Text": "GPU Core", + "Children": [], + "Min": "215 MHz", + "Value": "215 MHz", + "Max": "1683 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 36, + "Text": "GPU Memory", + "Children": [], + "Min": "405 MHz", + "Value": "405 MHz", + "Max": "5006 MHz", + "ImageURL": "images/transparent.png" + }, + { + "id": 37, + "Text": "GPU Shader", + "Children": [], + "Min": "430 MHz", + "Value": "430 MHz", + "Max": "3366 MHz", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/clock.png" + }, + { + "id": 38, + "Text": "Temperatures", + "Children": [ + { + "id": 39, + "Text": "GPU Core", + "Children": [], + "Min": "38.0 °C", + "Value": "39.0 °C", + "Max": "42.0 °C", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png" + }, + { + "id": 40, + "Text": "Load", + "Children": [ + { + "id": 41, + "Text": "GPU Core", + "Children": [], + "Min": "0.0 %", + "Value": "0.0 %", + "Max": "19.0 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 42, + "Text": "GPU Memory Controller", + "Children": [], + "Min": "0.0 %", + "Value": "0.0 %", + "Max": "2.0 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 43, + "Text": "GPU Video Engine", + "Children": [], + "Min": "0.0 %", + "Value": "0.0 %", + "Max": "0.0 %", + "ImageURL": "images/transparent.png" + }, + { + "id": 44, + "Text": "GPU Memory", + "Children": [], + "Min": "3.9 %", + "Value": "3.9 %", + "Max": "4.1 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png" + }, + { + "id": 45, + "Text": "Fans", + "Children": [ + { + "id": 46, + "Text": "GPU", + "Children": [], + "Min": "0 RPM", + "Value": "0 RPM", + "Max": "0 RPM", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/fan.png" + }, + { + "id": 47, + "Text": "Controls", + "Children": [ + { + "id": 48, + "Text": "GPU Fan", + "Children": [], + "Min": "0.0 %", + "Value": "0.0 %", + "Max": "0.0 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/control.png" + }, + { + "id": 49, + "Text": "Data", + "Children": [ + { + "id": 50, + "Text": "GPU Memory Free", + "Children": [], + "Min": "7854.8 MB", + "Value": "7873.1 MB", + "Max": "7873.1 MB", + "ImageURL": "images/transparent.png" + }, + { + "id": 51, + "Text": "GPU Memory Used", + "Children": [], + "Min": "318.9 MB", + "Value": "318.9 MB", + "Max": "337.2 MB", + "ImageURL": "images/transparent.png" + }, + { + "id": 52, + "Text": "GPU Memory Total", + "Children": [], + "Min": "8192.0 MB", + "Value": "8192.0 MB", + "Max": "8192.0 MB", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/power.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/nvidia.png" + }, + { + "id": 53, + "Text": "Generic Hard Disk", + "Children": [ + { + "id": 54, + "Text": "Load", + "Children": [ + { + "id": 55, + "Text": "Used Space", + "Children": [], + "Min": "74.6 %", + "Value": "75.3 %", + "Max": "75.6 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/hdd.png" + }, + { + "id": 56, + "Text": "WDC WD30EZRZ-00Z5HB0", + "Children": [ + { + "id": 57, + "Text": "Temperatures", + "Children": [ + { + "id": 58, + "Text": "Temperature", + "Children": [], + "Min": "30.0 °C", + "Value": "30.0 °C", + "Max": "32.0 °C", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png" + }, + { + "id": 59, + "Text": "Load", + "Children": [ + { + "id": 60, + "Text": "Used Space", + "Children": [], + "Min": "14.4 %", + "Value": "14.4 %", + "Max": "14.4 %", + "ImageURL": "images/transparent.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/hdd.png" + } + ], + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/computer.png" + } + ], + "Min": "Min", + "Value": "Value", + "Max": "Max", + "ImageURL": "" +} \ No newline at end of file