Initial commit

This commit is contained in:
Roger Gonzalez 2023-09-23 18:59:01 -03:00
commit c8d5c9115a
Signed by: rogs
GPG Key ID: C7ECE9C6C36EC2E6
15 changed files with 371 additions and 0 deletions

12
.gitignore vendored Normal file
View 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
View 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

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# UTE for Home Assistant
## Installation

View File

View 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

View 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
)

View File

@ -0,0 +1 @@
DOMAIN = "ute"

View 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"
}

View 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
View File

@ -0,0 +1,5 @@
{
"name": "UTE",
"render_readme": true,
"iot_class": "calculated"
}

3
requirements.test.txt Normal file
View File

@ -0,0 +1,3 @@
pytest
pytest-cov==2.9.0
pytest-homeassistant-custom-component

62
setup.cfg Normal file
View 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
View File

17
tests/bandit.yaml Normal file
View 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
View 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