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