Initial commit
This commit is contained in:
commit
c8d5c9115a
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
*.py[cod]
|
||||||
|
*.egg
|
||||||
|
build
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
script.py
|
58
.pre-commit-config.yaml
Normal file
58
.pre-commit-config.yaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py37-plus]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 19.10b0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
args:
|
||||||
|
- --safe
|
||||||
|
- --quiet
|
||||||
|
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v1.16.0
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
args:
|
||||||
|
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
|
||||||
|
- --skip="./.*,*.csv,*.json"
|
||||||
|
- --quiet-level=2
|
||||||
|
exclude_types: [csv, json]
|
||||||
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
|
rev: 3.8.1
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- flake8-docstrings==1.5.0
|
||||||
|
- pydocstyle==5.0.2
|
||||||
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
rev: 1.6.2
|
||||||
|
hooks:
|
||||||
|
- id: bandit
|
||||||
|
args:
|
||||||
|
- --quiet
|
||||||
|
- --format=custom
|
||||||
|
- --configfile=tests/bandit.yaml
|
||||||
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
|
rev: v4.3.21
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.4.0
|
||||||
|
hooks:
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
stages: [manual]
|
||||||
|
- id: check-json
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v0.770
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
args:
|
||||||
|
- --pretty
|
||||||
|
- --show-error-codes
|
||||||
|
- --show-error-context
|
0
custom_components/__init__.py
Normal file
0
custom_components/__init__.py
Normal file
26
custom_components/ute/__init__.py
Normal file
26
custom_components/ute/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up platform from a ConfigEntry."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||||
|
|
||||||
|
# Forward the setup to the sensor platform.
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, "sensor")
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
return True
|
89
custom_components/ute/config_flow.py
Normal file
89
custom_components/ute/config_flow.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_EMAIL
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_PHONE_NUMBER = "phone_number"
|
||||||
|
CONF_UTE_DEVICE_ID = "ute_device_id"
|
||||||
|
CONF_UTE_AVERAGE_COST_PER_KWH = "average_cost_per_kwh"
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
vol.Required(CONF_EMAIL): cv.string,
|
||||||
|
vol.Required(CONF_PHONE_NUMBER): cv.string,
|
||||||
|
vol.Optional(CONF_UTE_DEVICE_ID): cv.string,
|
||||||
|
vol.Optional(CONF_UTE_AVERAGE_COST_PER_KWH): cv.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_SCHEMA = vol.Schema(schema)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_email(email: str) -> None:
|
||||||
|
"""
|
||||||
|
Validates a email address
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: The email address to validate.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the email address is invalid.
|
||||||
|
"""
|
||||||
|
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_uyu_phone_number(phone_number: str) -> None:
|
||||||
|
"""
|
||||||
|
Validates a Uruguayan phone number
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone_number: The phone number to validate.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the phone number is invalid.
|
||||||
|
"""
|
||||||
|
if not phone_number.startswith("598"):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if not re.match(r"^[0-9]{11}$", phone_number):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
class UTEConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""
|
||||||
|
UTE Custom config flow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_entries: The config entries.
|
||||||
|
domain: The domain.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The config flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
|
||||||
|
"""Invoked when a user initiates a flow via the user interface."""
|
||||||
|
errors: Dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
validate_email(user_input[CONF_EMAIL])
|
||||||
|
validate_uyu_phone_number(user_input[CONF_PHONE_NUMBER])
|
||||||
|
except ValueError:
|
||||||
|
errors["base"] = "auth"
|
||||||
|
if not errors:
|
||||||
|
# Input is valid, set data.
|
||||||
|
self.data = user_input
|
||||||
|
return await self.async_step_repo()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=AUTH_SCHEMA, errors=errors
|
||||||
|
)
|
1
custom_components/ute/const.py
Normal file
1
custom_components/ute/const.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
DOMAIN = "ute"
|
11
custom_components/ute/manifest.json
Normal file
11
custom_components/ute/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"codeowners": ["@rogsme"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://github.com/rogsme/homeassistant_ute",
|
||||||
|
"domain": "ute",
|
||||||
|
"iot_class": "calculated",
|
||||||
|
"name": "UTE",
|
||||||
|
"requirements": ["ute-wrapper==1.0.3"],
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
75
custom_components/ute/sensor.py
Normal file
75
custom_components/ute/sensor.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from ute_wrapper.ute import UTEClient
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PHONE_NUMBER, CONF_UTE_DEVICE_ID, CONF_UTE_AVERAGE_COST_PER_KWH
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.typing import (
|
||||||
|
ConfigType,
|
||||||
|
DiscoveryInfoType,
|
||||||
|
HomeAssistantType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .config_flow import schema
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
# Time between updating data from UTE
|
||||||
|
SCAN_INTERVAL = timedelta(minutes=2)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: core.HomeAssistant,
|
||||||
|
config_entry: config_entries.ConfigEntry,
|
||||||
|
async_add_entities,
|
||||||
|
):
|
||||||
|
"""Setup sensors from a config entry created in the integrations UI."""
|
||||||
|
config = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
ute = UTEClient(
|
||||||
|
config[CONF_EMAIL],
|
||||||
|
config[CONF_PHONE_NUMBER],
|
||||||
|
config[CONF_UTE_DEVICE_ID],
|
||||||
|
config[CONF_UTE_AVERAGE_COST_PER_KWH],
|
||||||
|
)
|
||||||
|
sensor = UTESensor(ute)
|
||||||
|
async_add_entities(sensor, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
config: ConfigType,
|
||||||
|
async_add_entities: Callable,
|
||||||
|
discovery_info: Optional[DiscoveryInfoType] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensor platform."""
|
||||||
|
ute = UTEClient(
|
||||||
|
config[CONF_EMAIL],
|
||||||
|
config[CONF_PHONE_NUMBER],
|
||||||
|
config[CONF_UTE_DEVICE_ID],
|
||||||
|
config[CONF_UTE_AVERAGE_COST_PER_KWH],
|
||||||
|
)
|
||||||
|
sensor = UTESensor(ute)
|
||||||
|
async_add_entities(sensor, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UTESensor(Entity):
|
||||||
|
"""Representation of a UTE sensor."""
|
||||||
|
|
||||||
|
def __init__(self, ute: UTEClient):
|
||||||
|
super().__init__()
|
||||||
|
self.ute = ute
|
||||||
|
self._state = None
|
||||||
|
self._available = True
|
||||||
|
self._name = "Current energy usage"
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
try:
|
||||||
|
ute_data = await self.ute.get_current_usage_info()
|
||||||
|
self._state = ute_data["data"]["power_in_watts"]
|
||||||
|
except ():
|
||||||
|
pass
|
5
hacs.json
Normal file
5
hacs.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "UTE",
|
||||||
|
"render_readme": true,
|
||||||
|
"iot_class": "calculated"
|
||||||
|
}
|
3
requirements.test.txt
Normal file
3
requirements.test.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pytest
|
||||||
|
pytest-cov==2.9.0
|
||||||
|
pytest-homeassistant-custom-component
|
62
setup.cfg
Normal file
62
setup.cfg
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
[coverage:run]
|
||||||
|
source =
|
||||||
|
custom_components
|
||||||
|
|
||||||
|
[coverage:report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
raise NotImplemented()
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
show_missing = true
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
norecursedirs = .git
|
||||||
|
addopts =
|
||||||
|
--strict
|
||||||
|
--cov=custom_components
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
# https://github.com/ambv/black#line-length
|
||||||
|
max-line-length = 88
|
||||||
|
# E501: line too long
|
||||||
|
# W503: Line break occurred before a binary operator
|
||||||
|
# E203: Whitespace before ':'
|
||||||
|
# D202 No blank lines allowed after function docstring
|
||||||
|
# W504 line break after binary operator
|
||||||
|
ignore =
|
||||||
|
E501,
|
||||||
|
W503,
|
||||||
|
E203,
|
||||||
|
D202,
|
||||||
|
W504
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
# https://github.com/timothycrosley/isort
|
||||||
|
# https://github.com/timothycrosley/isort/wiki/isort-Settings
|
||||||
|
# splits long import on multiple lines indented by 4 spaces
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma=True
|
||||||
|
force_grid_wrap=0
|
||||||
|
use_parentheses=True
|
||||||
|
line_length=88
|
||||||
|
indent = " "
|
||||||
|
# by default isort don't check module indexes
|
||||||
|
not_skip = __init__.py
|
||||||
|
# will group `import x` and `from x import` of the same module.
|
||||||
|
force_sort_within_sections = true
|
||||||
|
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||||
|
default_section = THIRDPARTY
|
||||||
|
known_first_party = custom_components,tests
|
||||||
|
forced_separate = tests
|
||||||
|
combine_as_imports = true
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
python_version = 3.7
|
||||||
|
ignore_errors = true
|
||||||
|
follow_imports = silent
|
||||||
|
ignore_missing_imports = true
|
||||||
|
warn_incomplete_stub = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_configs = true
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
17
tests/bandit.yaml
Normal file
17
tests/bandit.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# https://bandit.readthedocs.io/en/latest/config.html
|
||||||
|
|
||||||
|
tests:
|
||||||
|
- B108
|
||||||
|
- B306
|
||||||
|
- B307
|
||||||
|
- B313
|
||||||
|
- B314
|
||||||
|
- B315
|
||||||
|
- B316
|
||||||
|
- B317
|
||||||
|
- B318
|
||||||
|
- B319
|
||||||
|
- B320
|
||||||
|
- B325
|
||||||
|
- B602
|
||||||
|
- B604
|
9
tests/test_init.py
Normal file
9
tests/test_init.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""Test component setup."""
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from custom_components.ute.const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup(hass):
|
||||||
|
"""Test the component gets setup."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {}) is True
|
Reference in New Issue
Block a user