mirror of
https://github.com/home-assistant/core.git
synced 2025-09-06 21:31:34 +02:00
Migrate Emoncms_history to external async library (#149824)
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -422,6 +422,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/emby/ @mezz64
|
||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||
/homeassistant/components/emoncms_history/ @alexandrecuer
|
||||
/tests/components/emoncms_history/ @alexandrecuer
|
||||
/homeassistant/components/emonitor/ @bdraco
|
||||
/tests/components/emonitor/ @bdraco
|
||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||
|
@@ -1,10 +1,11 @@
|
||||
"""Support for sending data to Emoncms."""
|
||||
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import aiohttp
|
||||
from pyemoncms import EmoncmsClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -17,9 +18,9 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,61 +43,51 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_send_to_emoncms(
|
||||
hass: HomeAssistant,
|
||||
emoncms_client: EmoncmsClient,
|
||||
whitelist: list[str],
|
||||
node: str | int,
|
||||
_: datetime,
|
||||
) -> None:
|
||||
"""Send data to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
||||
continue
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if payload_dict:
|
||||
try:
|
||||
await emoncms_client.async_input_post(data=payload_dict, node=node)
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOGGER.warning("Network error when sending data to Emoncms: %s", err)
|
||||
except ValueError as err:
|
||||
_LOGGER.warning("Value error when preparing data for Emoncms: %s", err)
|
||||
else:
|
||||
_LOGGER.debug("Sent data to Emoncms: %s", payload_dict)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Emoncms history component."""
|
||||
conf = config[DOMAIN]
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
input_node = str(conf.get(CONF_INPUTNODE))
|
||||
|
||||
def send_data(url, apikey, node, payload):
|
||||
"""Send payload data to Emoncms."""
|
||||
try:
|
||||
fullurl = f"{url}/input/post.json"
|
||||
data = {"apikey": apikey, "data": payload}
|
||||
parameters = {"node": node}
|
||||
req = requests.post(
|
||||
fullurl, params=parameters, data=data, allow_redirects=True, timeout=5
|
||||
)
|
||||
emoncms_client = EmoncmsClient(
|
||||
url=conf.get(CONF_URL),
|
||||
api_key=conf.get(CONF_API_KEY),
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
async_track_time_interval(
|
||||
hass,
|
||||
partial(async_send_to_emoncms, hass, emoncms_client, whitelist, input_node),
|
||||
timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)),
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl)
|
||||
|
||||
else:
|
||||
if req.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error(
|
||||
"Error saving data %s to %s (http status code = %d)",
|
||||
payload,
|
||||
fullurl,
|
||||
req.status_code,
|
||||
)
|
||||
|
||||
def update_emoncms(time):
|
||||
"""Send whitelisted entities states regularly to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
||||
continue
|
||||
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if payload_dict:
|
||||
payload = ",".join(f"{key}:{val}" for key, val in payload_dict.items())
|
||||
|
||||
send_data(
|
||||
conf.get(CONF_URL),
|
||||
conf.get(CONF_API_KEY),
|
||||
str(conf.get(CONF_INPUTNODE)),
|
||||
f"{{{payload}}}",
|
||||
)
|
||||
|
||||
track_point_in_time(
|
||||
hass, update_emoncms, time + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))
|
||||
)
|
||||
|
||||
update_emoncms(dt_util.utcnow())
|
||||
return True
|
||||
|
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"domain": "emoncms_history",
|
||||
"name": "Emoncms History",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@alexandrecuer"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/emoncms_history",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy"
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyemoncms==0.1.2"]
|
||||
}
|
||||
|
1
requirements_all.txt
generated
1
requirements_all.txt
generated
@@ -1957,6 +1957,7 @@ pyefergy==22.5.0
|
||||
pyegps==0.2.5
|
||||
|
||||
# homeassistant.components.emoncms
|
||||
# homeassistant.components.emoncms_history
|
||||
pyemoncms==0.1.2
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
|
1
requirements_test_all.txt
generated
1
requirements_test_all.txt
generated
@@ -1632,6 +1632,7 @@ pyefergy==22.5.0
|
||||
pyegps==0.2.5
|
||||
|
||||
# homeassistant.components.emoncms
|
||||
# homeassistant.components.emoncms_history
|
||||
pyemoncms==0.1.2
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
|
1
tests/components/emoncms_history/__init__.py
Normal file
1
tests/components/emoncms_history/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for emoncms_history component."""
|
125
tests/components/emoncms_history/test_init.py
Normal file
125
tests/components/emoncms_history/test_init.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""The tests for the emoncms_history init."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import aiohttp
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_setup_valid_config(hass: HomeAssistant) -> None:
|
||||
"""Test setting up the emoncms_history component with valid configuration."""
|
||||
config = {
|
||||
"emoncms_history": {
|
||||
CONF_API_KEY: "dummy",
|
||||
CONF_URL: "https://emoncms.example",
|
||||
"inputnode": 42,
|
||||
"whitelist": ["sensor.temp"],
|
||||
}
|
||||
}
|
||||
# Simulate a sensor
|
||||
hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await async_setup_component(hass, "emoncms_history", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_setup_missing_config(hass: HomeAssistant) -> None:
|
||||
"""Test setting up the emoncms_history component with missing configuration."""
|
||||
config = {"emoncms_history": {"api_key": "dummy"}}
|
||||
success = await async_setup_component(hass, "emoncms_history", config)
|
||||
assert not success
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def emoncms_client() -> AsyncGenerator[AsyncMock]:
|
||||
"""Mock pyemoncms client with successful responses."""
|
||||
with patch(
|
||||
"homeassistant.components.emoncms_history.EmoncmsClient", autospec=True
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
client.async_input_post.return_value = '{"success": true}'
|
||||
yield client
|
||||
|
||||
|
||||
async def test_emoncms_send_data(
|
||||
hass: HomeAssistant,
|
||||
emoncms_client: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test sending data to Emoncms with and without success."""
|
||||
|
||||
config = {
|
||||
"emoncms_history": {
|
||||
"api_key": "dummy",
|
||||
"url": "http://fake-url",
|
||||
"inputnode": 42,
|
||||
"whitelist": ["sensor.temp"],
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "emoncms_history", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for state in None, "", STATE_UNAVAILABLE, STATE_UNKNOWN:
|
||||
hass.states.async_set("sensor.temp", state, {"unit_of_measurement": "°C"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert emoncms_client.async_input_post.call_args is None
|
||||
|
||||
hass.states.async_set("sensor.temp", "not_a_number", {"unit_of_measurement": "°C"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
emoncms_client.async_input_post.assert_not_called()
|
||||
|
||||
hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
emoncms_client.async_input_post.assert_called_once()
|
||||
assert emoncms_client.async_input_post.return_value == '{"success": true}'
|
||||
|
||||
_, kwargs = emoncms_client.async_input_post.call_args
|
||||
assert kwargs["data"] == {"sensor.temp": 23.4}
|
||||
assert kwargs["node"] == "42"
|
||||
|
||||
emoncms_client.async_input_post.side_effect = aiohttp.ClientError(
|
||||
"Connection refused"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(
|
||||
"Network error when sending data to Emoncms" in message
|
||||
for message in caplog.text.splitlines()
|
||||
)
|
||||
|
||||
emoncms_client.async_input_post.side_effect = ValueError("Invalid value format")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(
|
||||
"Value error when preparing data for Emoncms" in message
|
||||
for message in caplog.text.splitlines()
|
||||
)
|
Reference in New Issue
Block a user