mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Smart Meter Texas integration (#37966)
* Run scaffold script * Update version * Bump version * Initial commit * Move meter and ESIID to device attributes * Update internal to hourly due to api limit * Format with Black * Fix typo * Update tests * Update description * Disable Pylint error * Don't commit translations * Remove meter number from sensor name * Allow multiple meters per account * Move data updates to a DataUpdateCoordinator * Use setdefault to setup the component * Move strings to const.py * Fix tests * Remove meter last updated attribute * Bump smart-meter-texas version * Fix logger call Co-authored-by: J. Nick Koston <nick@koston.org> * Remove unneeded manifest keys Co-authored-by: J. Nick Koston <nick@koston.org> * Remove icon property Co-authored-by: J. Nick Koston <nick@koston.org> * Handle instance where user already setup an account Co-authored-by: J. Nick Koston <nick@koston.org> * Remove icon constant * Fix indentation * Handle config flow errors better * Use ESIID + meter number as unique ID for sensor * Update config flow tests to reach 100% coverage * Avoid reading meters on startup Cherrypick @bdraco's suggestion * Run scaffold script * Update version * Bump version * Initial commit * Move meter and ESIID to device attributes * Update internal to hourly due to api limit * Format with Black * Fix typo * Update tests * Update description * Disable Pylint error * Don't commit translations * Remove meter number from sensor name * Allow multiple meters per account * Move data updates to a DataUpdateCoordinator * Use setdefault to setup the component * Move strings to const.py * Fix tests * Remove meter last updated attribute * Bump smart-meter-texas version * Fix logger call Co-authored-by: J. Nick Koston <nick@koston.org> * Remove unneeded manifest keys Co-authored-by: J. Nick Koston <nick@koston.org> * Remove icon property Co-authored-by: J. Nick Koston <nick@koston.org> * Handle instance where user already setup an account Co-authored-by: J. Nick Koston <nick@koston.org> * Remove icon constant * Fix indentation * Handle config flow errors better * Use ESIID + meter number as unique ID for sensor * Update config flow tests to reach 100% coverage * Remove unnecessary try/except block This checks for the same exception just prior in execution on L51. * Remove unused return values * Add tests * Improve tests and coverage * Use more pythonic control flow * Remove all uses of hass.data Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
@ -377,6 +377,7 @@ homeassistant/components/sky_hub/* @rogerselwyn
|
||||
homeassistant/components/slide/* @ualex73
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smappee/* @bsmappee
|
||||
homeassistant/components/smart_meter_texas/* @grahamwetzler
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smarty/* @z0mbieprocess
|
||||
|
133
homeassistant/components/smart_meter_texas/__init__.py
Normal file
133
homeassistant/components/smart_meter_texas/__init__.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""The Smart Meter Texas integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from smart_meter_texas import Account, Client
|
||||
from smart_meter_texas.exceptions import (
|
||||
SmartMeterTexasAPIError,
|
||||
SmartMeterTexasAuthError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
Debouncer,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DATA_COORDINATOR,
|
||||
DATA_SMART_METER,
|
||||
DEBOUNCE_COOLDOWN,
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["sensor"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Smart Meter Texas component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Smart Meter Texas from a config entry."""
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
account = Account(username, password)
|
||||
smartmetertexas = SmartMeterTexasData(hass, entry, account)
|
||||
try:
|
||||
await smartmetertexas.client.authenticate()
|
||||
except SmartMeterTexasAuthError:
|
||||
_LOGGER.error("Username or password was not accepted")
|
||||
return False
|
||||
except asyncio.TimeoutError:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
await smartmetertexas.setup()
|
||||
|
||||
async def async_update_data():
|
||||
_LOGGER.debug("Fetching latest data")
|
||||
await smartmetertexas.read_meters()
|
||||
return smartmetertexas
|
||||
|
||||
# Use a DataUpdateCoordinator to manage the updates. This is due to the
|
||||
# Smart Meter Texas API which takes around 30 seconds to read a meter.
|
||||
# This avoids Home Assistant from complaining about the component taking
|
||||
# too long to update.
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Smart Meter Texas",
|
||||
update_method=async_update_data,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_COOLDOWN, immediate=True
|
||||
),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_COORDINATOR: coordinator,
|
||||
DATA_SMART_METER: smartmetertexas,
|
||||
}
|
||||
|
||||
asyncio.create_task(coordinator.async_refresh())
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SmartMeterTexasData:
|
||||
"""Manages coordinatation of API data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, account: Account):
|
||||
"""Initialize the data coordintator."""
|
||||
self._entry = entry
|
||||
self.account = account
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
self.client = Client(websession, account)
|
||||
self.meters = []
|
||||
|
||||
async def setup(self):
|
||||
"""Fetch all of the user's meters."""
|
||||
self.meters = await self.account.fetch_meters(self.client)
|
||||
_LOGGER.debug("Discovered %s meter(s)", len(self.meters))
|
||||
|
||||
async def read_meters(self):
|
||||
"""Read each meter."""
|
||||
for meter in self.meters:
|
||||
try:
|
||||
await meter.read_meter(self.client)
|
||||
except (SmartMeterTexasAPIError, SmartMeterTexasAuthError) as error:
|
||||
raise UpdateFailed(error)
|
||||
return self.meters
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
91
homeassistant/components/smart_meter_texas/config_flow.py
Normal file
91
homeassistant/components/smart_meter_texas/config_flow.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Config flow for Smart Meter Texas integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from smart_meter_texas import Account, Client
|
||||
from smart_meter_texas.exceptions import (
|
||||
SmartMeterTexasAPIError,
|
||||
SmartMeterTexasAuthError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
client_session = aiohttp_client.async_get_clientsession(hass)
|
||||
account = Account(data["username"], data["password"])
|
||||
client = Client(client_session, account)
|
||||
|
||||
try:
|
||||
await client.authenticate()
|
||||
except (asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError):
|
||||
raise CannotConnect
|
||||
except SmartMeterTexasAuthError as error:
|
||||
raise InvalidAuth(error)
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": account.username}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Smart Meter Texas."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def _account_already_configured(self, account):
|
||||
existing_accounts = {
|
||||
entry.data[CONF_USERNAME]
|
||||
for entry in self._async_current_entries()
|
||||
if CONF_USERNAME in entry.data
|
||||
}
|
||||
return account in existing_accounts
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if not errors:
|
||||
if self._account_already_configured(user_input[CONF_USERNAME]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
15
homeassistant/components/smart_meter_texas/const.py
Normal file
15
homeassistant/components/smart_meter_texas/const.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Constants for the Smart Meter Texas integration."""
|
||||
from datetime import timedelta
|
||||
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
DEBOUNCE_COOLDOWN = 1800 # Seconds
|
||||
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_SMART_METER = "smart_meter_data"
|
||||
|
||||
DOMAIN = "smart_meter_texas"
|
||||
|
||||
METER_NUMBER = "meter_number"
|
||||
ESIID = "electric_service_identifier"
|
||||
LAST_UPDATE = "last_updated"
|
||||
ELECTRIC_METER = "Electric Meter"
|
8
homeassistant/components/smart_meter_texas/manifest.json
Normal file
8
homeassistant/components/smart_meter_texas/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "smart_meter_texas",
|
||||
"name": "Smart Meter Texas",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/smart_meter_texas",
|
||||
"requirements": ["smart-meter-texas==0.4.0"],
|
||||
"codeowners": ["@grahamwetzler"]
|
||||
}
|
112
homeassistant/components/smart_meter_texas/sensor.py
Normal file
112
homeassistant/components/smart_meter_texas/sensor.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Support for Smart Meter Texas sensors."""
|
||||
import logging
|
||||
|
||||
from smart_meter_texas import Meter
|
||||
|
||||
from homeassistant.const import CONF_ADDRESS, ENERGY_KILO_WATT_HOUR
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
DATA_COORDINATOR,
|
||||
DATA_SMART_METER,
|
||||
DOMAIN,
|
||||
ELECTRIC_METER,
|
||||
ESIID,
|
||||
METER_NUMBER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Smart Meter Texas sensors."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
meters = hass.data[DOMAIN][config_entry.entry_id][DATA_SMART_METER].meters
|
||||
|
||||
async_add_entities(
|
||||
[SmartMeterTexasSensor(meter, coordinator) for meter in meters], False
|
||||
)
|
||||
|
||||
|
||||
class SmartMeterTexasSensor(RestoreEntity, Entity):
|
||||
"""Representation of an Smart Meter Texas sensor."""
|
||||
|
||||
def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator):
|
||||
"""Initialize the sensor."""
|
||||
self.meter = meter
|
||||
self.coordinator = coordinator
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return ENERGY_KILO_WATT_HOUR
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Device Name."""
|
||||
return f"{ELECTRIC_METER} {self.meter.meter}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Device Uniqueid."""
|
||||
return f"{self.meter.esiid}_{self.meter.meter}"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Get the latest reading."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
attributes = {
|
||||
METER_NUMBER: self.meter.meter,
|
||||
ESIID: self.meter.esiid,
|
||||
CONF_ADDRESS: self.meter.address,
|
||||
}
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via coordinator."""
|
||||
return False
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _state_update(self):
|
||||
"""Call when the coordinator has an update."""
|
||||
self._available = self.coordinator.last_update_success
|
||||
if self._available:
|
||||
self._state = self.meter.reading
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to updates."""
|
||||
self.async_on_remove(self.coordinator.async_add_listener(self._state_update))
|
||||
|
||||
# If the background update finished before
|
||||
# we added the entity, there is no need to restore
|
||||
# state.
|
||||
if self.coordinator.last_update_success:
|
||||
return
|
||||
|
||||
last_state = await self.async_get_last_state()
|
||||
if last_state:
|
||||
self._state = last_state.state
|
||||
self._available = True
|
22
homeassistant/components/smart_meter_texas/strings.json
Normal file
22
homeassistant/components/smart_meter_texas/strings.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"title": "Smart Meter Texas",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Provide your username and password for Smart Meter Texas.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -151,6 +151,7 @@ FLOWS = [
|
||||
"shopping_list",
|
||||
"simplisafe",
|
||||
"smappee",
|
||||
"smart_meter_texas",
|
||||
"smarthab",
|
||||
"smartthings",
|
||||
"smhi",
|
||||
|
@ -1993,6 +1993,9 @@ sleepyq==0.7
|
||||
# homeassistant.components.xmpp
|
||||
slixmpp==1.5.1
|
||||
|
||||
# homeassistant.components.smart_meter_texas
|
||||
smart-meter-texas==0.4.0
|
||||
|
||||
# homeassistant.components.smarthab
|
||||
smarthab==0.21
|
||||
|
||||
|
@ -899,6 +899,9 @@ simplisafe-python==9.3.0
|
||||
# homeassistant.components.sleepiq
|
||||
sleepyq==0.7
|
||||
|
||||
# homeassistant.components.smart_meter_texas
|
||||
smart-meter-texas==0.4.0
|
||||
|
||||
# homeassistant.components.smarthab
|
||||
smarthab==0.21
|
||||
|
||||
|
1
tests/components/smart_meter_texas/__init__.py
Normal file
1
tests/components/smart_meter_texas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Smart Meter Texas integration."""
|
100
tests/components/smart_meter_texas/conftest.py
Normal file
100
tests/components/smart_meter_texas/conftest.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""Test configuration and mocks for Smart Meter Texas."""
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from smart_meter_texas.const import (
|
||||
AUTH_ENDPOINT,
|
||||
BASE_ENDPOINT,
|
||||
BASE_URL,
|
||||
LATEST_OD_READ_ENDPOINT,
|
||||
METER_ENDPOINT,
|
||||
OD_READ_ENDPOINT,
|
||||
)
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.smart_meter_texas.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
TEST_ENTITY_ID = "sensor.electric_meter_123456789"
|
||||
|
||||
|
||||
def load_smt_fixture(name):
|
||||
"""Return a dict of the json fixture."""
|
||||
json_fixture = load_fixture(Path() / DOMAIN / f"{name}.json")
|
||||
return json.loads(json_fixture)
|
||||
|
||||
|
||||
async def setup_integration(hass, config_entry, aioclient_mock):
|
||||
"""Initialize the Smart Meter Texas integration for testing."""
|
||||
mock_connection(aioclient_mock)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def refresh_data(hass, config_entry, aioclient_mock):
|
||||
"""Request a DataUpdateCoordinator refresh."""
|
||||
mock_connection(aioclient_mock)
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def mock_connection(
|
||||
aioclient_mock, auth_fail=False, auth_timeout=False, bad_reading=False
|
||||
):
|
||||
"""Mock all calls to the API."""
|
||||
aioclient_mock.get(BASE_URL)
|
||||
|
||||
auth_endpoint = f"{BASE_ENDPOINT}{AUTH_ENDPOINT}"
|
||||
if not auth_fail and not auth_timeout:
|
||||
aioclient_mock.post(
|
||||
auth_endpoint, json={"token": "token123"},
|
||||
)
|
||||
elif auth_fail:
|
||||
aioclient_mock.post(
|
||||
auth_endpoint,
|
||||
status=400,
|
||||
json={"errormessage": "ERR-USR-INVALIDPASSWORDERROR"},
|
||||
)
|
||||
else: # auth_timeout
|
||||
aioclient_mock.post(auth_endpoint, exc=asyncio.TimeoutError)
|
||||
|
||||
aioclient_mock.post(
|
||||
f"{BASE_ENDPOINT}{METER_ENDPOINT}", json=load_smt_fixture("meter"),
|
||||
)
|
||||
aioclient_mock.post(f"{BASE_ENDPOINT}{OD_READ_ENDPOINT}", json={"data": None})
|
||||
if not bad_reading:
|
||||
aioclient_mock.post(
|
||||
f"{BASE_ENDPOINT}{LATEST_OD_READ_ENDPOINT}",
|
||||
json=load_smt_fixture("latestodrread"),
|
||||
)
|
||||
else:
|
||||
aioclient_mock.post(
|
||||
f"{BASE_ENDPOINT}{LATEST_OD_READ_ENDPOINT}", json={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry(hass):
|
||||
"""Return a mock config entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="user123",
|
||||
data={"username": "user123", "password": "password123"},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
return config_entry
|
120
tests/components/smart_meter_texas/test_config_flow.py
Normal file
120
tests/components/smart_meter_texas/test_config_flow.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""Test the Smart Meter Texas config flow."""
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientError
|
||||
import pytest
|
||||
from smart_meter_texas.exceptions import (
|
||||
SmartMeterTexasAPIError,
|
||||
SmartMeterTexasAuthError,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.smart_meter_texas.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_LOGIN = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch("smart_meter_texas.Client.authenticate", return_value=True), patch(
|
||||
"homeassistant.components.smart_meter_texas.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.smart_meter_texas.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_LOGIN
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == TEST_LOGIN[CONF_USERNAME]
|
||||
assert result2["data"] == TEST_LOGIN
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass):
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"smart_meter_texas.Client.authenticate", side_effect=SmartMeterTexasAuthError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_LOGIN,
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect", [asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError]
|
||||
)
|
||||
async def test_form_cannot_connect(hass, side_effect):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"smart_meter_texas.Client.authenticate", side_effect=side_effect,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_LOGIN
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_exception(hass):
|
||||
"""Test base exception is handled."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"smart_meter_texas.Client.authenticate", side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_LOGIN,
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_duplicate_account(hass):
|
||||
"""Test that a duplicate account cannot be configured."""
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="user123",
|
||||
data={"username": "user123", "password": "password123"},
|
||||
).add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"smart_meter_texas.Client.authenticate", return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={"username": "user123", "password": "password123"},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
74
tests/components/smart_meter_texas/test_init.py
Normal file
74
tests/components/smart_meter_texas/test_init.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Test the Smart Meter Texas module."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.smart_meter_texas import async_setup_entry
|
||||
from homeassistant.components.smart_meter_texas.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import TEST_ENTITY_ID, mock_connection, setup_integration
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
async def test_setup_with_no_config(hass):
|
||||
"""Test that no config is successful."""
|
||||
assert await async_setup_component(hass, DOMAIN, {}) is True
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert no flows were started.
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
||||
|
||||
async def test_auth_failure(hass, config_entry, aioclient_mock):
|
||||
"""Test if user's username or password is not accepted."""
|
||||
mock_connection(aioclient_mock, auth_fail=True)
|
||||
result = await async_setup_entry(hass, config_entry)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
async def test_api_timeout(hass, config_entry, aioclient_mock):
|
||||
"""Test that a timeout results in ConfigEntryNotReady."""
|
||||
mock_connection(aioclient_mock, auth_timeout=True)
|
||||
with pytest.raises(ConfigEntryNotReady):
|
||||
await async_setup_entry(hass, config_entry)
|
||||
|
||||
assert config_entry.state == ENTRY_STATE_NOT_LOADED
|
||||
|
||||
|
||||
async def test_update_failure(hass, config_entry, aioclient_mock):
|
||||
"""Test that the coordinator handles a bad response."""
|
||||
mock_connection(aioclient_mock, bad_reading=True)
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
with patch("smart_meter_texas.Meter.read_meter") as updater:
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
updater.assert_called_once()
|
||||
|
||||
|
||||
async def test_unload_config_entry(hass, config_entry, aioclient_mock):
|
||||
"""Test entry unloading."""
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0] is config_entry
|
||||
assert config_entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ENTRY_STATE_NOT_LOADED
|
65
tests/components/smart_meter_texas/test_sensor.py
Normal file
65
tests/components/smart_meter_texas/test_sensor.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Test the Smart Meter Texas sensor entity."""
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.smart_meter_texas.const import (
|
||||
ELECTRIC_METER,
|
||||
ESIID,
|
||||
METER_NUMBER,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ADDRESS
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import TEST_ENTITY_ID, mock_connection, refresh_data, setup_integration
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
async def test_sensor(hass, config_entry, aioclient_mock):
|
||||
"""Test that the sensor is setup."""
|
||||
mock_connection(aioclient_mock)
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
await refresh_data(hass, config_entry, aioclient_mock)
|
||||
meter = hass.states.get(TEST_ENTITY_ID)
|
||||
|
||||
assert meter
|
||||
assert meter.state == "9751.212"
|
||||
|
||||
|
||||
async def test_name(hass, config_entry, aioclient_mock):
|
||||
"""Test sensor name property."""
|
||||
mock_connection(aioclient_mock)
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
await refresh_data(hass, config_entry, aioclient_mock)
|
||||
meter = hass.states.get(TEST_ENTITY_ID)
|
||||
|
||||
assert meter.name == f"{ELECTRIC_METER} 123456789"
|
||||
|
||||
|
||||
async def test_attributes(hass, config_entry, aioclient_mock):
|
||||
"""Test meter attributes."""
|
||||
mock_connection(aioclient_mock)
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
await refresh_data(hass, config_entry, aioclient_mock)
|
||||
meter = hass.states.get(TEST_ENTITY_ID)
|
||||
|
||||
assert meter.attributes[METER_NUMBER] == "123456789"
|
||||
assert meter.attributes[ESIID] == "12345678901234567"
|
||||
assert meter.attributes[CONF_ADDRESS] == "123 MAIN ST"
|
||||
|
||||
|
||||
async def test_generic_entity_update_service(hass, config_entry, aioclient_mock):
|
||||
"""Test generic update entity service homeasasistant/update_entity."""
|
||||
mock_connection(aioclient_mock)
|
||||
await setup_integration(hass, config_entry, aioclient_mock)
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
with patch("smart_meter_texas.Meter.read_meter") as updater:
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
updater.assert_called_once()
|
9
tests/fixtures/smart_meter_texas/latestodrread.json
vendored
Normal file
9
tests/fixtures/smart_meter_texas/latestodrread.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"data": {
|
||||
"odrstatus": "COMPLETED",
|
||||
"odrread": "9751.212",
|
||||
"odrusage": "43.826",
|
||||
"odrdate": "08/15/2020 14:07:40",
|
||||
"responseMessage": "SUCCESS"
|
||||
}
|
||||
}
|
22
tests/fixtures/smart_meter_texas/meter.json
vendored
Normal file
22
tests/fixtures/smart_meter_texas/meter.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"customer": "X",
|
||||
"email": "Y",
|
||||
"description": "123 Main St",
|
||||
"address": "123 MAIN ST",
|
||||
"city": "DALLAS",
|
||||
"state": "TX",
|
||||
"zip": "75001-0.00",
|
||||
"esiid": "12345678901234567",
|
||||
"meterNumber": "123456789",
|
||||
"meterMultiplier": 1,
|
||||
"fullAddress": "123 MAIN ST, DALLAS, TX, 75001-0.00",
|
||||
"errmsg": "Success",
|
||||
"recordStatus": "ESIID-RECORD-EXISTS",
|
||||
"statusIndicator": true,
|
||||
"dunsNumber": "NO_DUNS"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user