Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,66 @@ jobs:
- name: "Install requirements"
run: python3 -m pip install -r requirements.txt

- name: "Run"
- name: "Run Ruff"
run: python3 -m ruff check .

mypy:
name: "Type Check"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout the repository"
uses: "actions/checkout@v6.0.2"

- name: "Set up Python"
uses: actions/setup-python@v6.2.0
with:
python-version: "3.11"
cache: "pip"

- name: "Install requirements"
run: python3 -m pip install -r requirements.txt

- name: "Run MyPy"
run: python3 -m mypy custom_components/iec

autofix:
name: "Auto-fix"
runs-on: "ubuntu-latest"
if: github.event_name == 'pull_request'
permissions:
contents: write
steps:
- name: "Checkout the repository"
uses: "actions/checkout@v6.0.2"
with:
ref: ${{ github.head_ref }}

- name: "Set up Python"
uses: actions/setup-python@v6.2.0
with:
python-version: "3.11"
cache: "pip"

- name: "Install requirements"
run: python3 -m pip install -r requirements.txt

- name: "Auto-fix with Ruff"
run: python3 -m ruff check . --fix

- name: "Check for changes"
id: verify-changed-files
run: |
if git diff --quiet; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi

- name: "Commit changes"
if: steps.verify-changed-files.outputs.changed == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "style: auto-fix ruff linting issues"
git push
64 changes: 41 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Github is used for everything

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
4. Test you contribution.
5. Issue that pull request!
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Development Setup

To set up your development environment:

1. Fork the repo and clone it locally
2. Run `./scripts/setup` to install dependencies and set up git hooks
3. The pre-commit hook will automatically run linting and type checking on every commit

## Github is used for everything

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
4. Test you contribution.
5. Issue that pull request!

## Any contributions you make will be under the MIT Software License

Expand All @@ -42,9 +50,19 @@ Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!

People *love* thorough bug reports. I'm not even kidding.

## Use a Consistent Coding Style

Use [black](https://github.com/ambv/black) to make sure the code follows the style.
## Use a Consistent Coding Style

This project uses:
- **[Ruff](https://github.com/astral-sh/ruff)** for linting and code formatting
- **[MyPy](https://github.com/python/mypy)** for static type checking

The pre-commit hook will automatically run these checks before each commit. You can also run them manually:

```bash
./scripts/lint # Run both ruff and mypy
./scripts/typecheck # Run mypy only
ruff check . --fix # Auto-fix linting issues
```

## Test your code modification

Expand Down
6 changes: 4 additions & 2 deletions custom_components/iec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations
import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
Expand Down Expand Up @@ -30,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# Register the debug service
async def handle_debug_get_coordinator_data(call) -> None: # noqa: ANN001 ARG001
async def handle_debug_get_coordinator_data(call: Any) -> None:
# Log or return coordinator data
data = iec_coordinator.data
_LOGGER.info("Coordinator data: %s", data)
Expand All @@ -45,7 +46,8 @@ async def handle_debug_get_coordinator_data(call) -> None: # noqa: ANN001 ARG00

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id, None)
if coordinator:
await coordinator.async_unload()
Expand Down
16 changes: 9 additions & 7 deletions custom_components/iec/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
Expand Down Expand Up @@ -35,7 +36,7 @@
class IecBinaryEntityDescriptionMixin:
"""Mixin values for required keys."""

value_fn: Callable[dict, bool | None]
value_fn: Callable[[Any], bool | None]


@dataclass(frozen=True, kw_only=True)
Expand Down Expand Up @@ -110,8 +111,8 @@ def __init__(
entity_description: IecBinarySensorEntityDescription,
contract_id: str,
is_multi_contract: bool,
attributes_to_add: dict | None = None,
):
attributes_to_add: dict[str, Any] | None = None,
) -> None:
"""Initialize the sensor."""
super().__init__(
coordinator,
Expand All @@ -122,7 +123,7 @@ def __init__(
self.entity_description = entity_description
self._attr_unique_id = f"{str(contract_id)}_{entity_description.key}"

attributes = {"contract_id": contract_id}
attributes: dict[str, Any] = {"contract_id": contract_id}

if attributes_to_add:
attributes.update(attributes_to_add)
Expand All @@ -140,9 +141,10 @@ def __init__(
@property
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.data.get(self.contract_id)
)
contract_data = self.coordinator.data.get(self.contract_id)
if contract_data is None:
return None
return self.entity_description.value_fn(contract_data)

@property
def device_info(self) -> DeviceInfo:
Expand Down
9 changes: 5 additions & 4 deletions custom_components/iec/commons.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
"""IEC common functions."""

import pytz

from datetime import date
from enum import Enum

import pytz
from homeassistant.helpers.device_registry import DeviceInfo
from iec_api.models.remote_reading import PeriodConsumption

from custom_components.iec import DOMAIN
from .const import DOMAIN

TIMEZONE = pytz.timezone("Asia/Jerusalem")

Expand All @@ -29,7 +28,9 @@ def find_reading_by_date(daily_reading: PeriodConsumption, desired_date: date) -
TypeError: If the `daily_reading.date` attribute is not of type `datetime`.

"""
return daily_reading.interval.date() == desired_date # Checks if the dates match
return bool(
daily_reading.interval.date() == desired_date
) # Checks if the dates match


class IecEntityType(Enum):
Expand Down
36 changes: 19 additions & 17 deletions custom_components/iec/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ async def _validate_login(
return errors


class IecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class IecConfigFlow(config_entries.ConfigFlow):
"""Handle a config flow for IEC."""

VERSION = 1
DOMAIN = DOMAIN

def __init__(self) -> None:
"""Initialize a new IECConfigFlow."""
Expand Down Expand Up @@ -139,6 +140,7 @@ async def async_step_mfa(
if data.get(CONF_TOTP_SECRET):
data.pop(CONF_TOTP_SECRET)

contract_ids: list[int] = []
try:
customer = await client.get_customer()
data[CONF_BP_NUMBER] = customer.bp_number
Expand Down Expand Up @@ -221,19 +223,18 @@ async def async_step_select_contracts(
assert self.data.get(CONF_BP_NUMBER) is not None

errors: dict[str, str] = {}
if (
user_input is not None
and user_input.get(CONF_SELECTED_CONTRACTS) is not None
):
if len(user_input.get(CONF_SELECTED_CONTRACTS)) == 0:
errors["base"] = "no_contracts"
else:
data = {**self.data, **user_input}
if data.get(CONF_AVAILABLE_CONTRACTS):
data.pop(CONF_AVAILABLE_CONTRACTS)

self.data = data
return self._async_create_iec_entry(data)
if user_input is not None:
selected_contracts = user_input.get(CONF_SELECTED_CONTRACTS)
if selected_contracts is not None:
if len(selected_contracts) == 0:
errors["base"] = "no_contracts"
else:
data = {**self.data, **user_input}
if data.get(CONF_AVAILABLE_CONTRACTS):
data.pop(CONF_AVAILABLE_CONTRACTS)

self.data = data
return self._async_create_iec_entry(data)

schema = {
vol.Required(
Expand Down Expand Up @@ -280,9 +281,10 @@ async def async_step_reauth_confirm(
return self.async_abort(reason="reauth_successful")

if not client:
self.client = IecClient(
self.data[CONF_USER_ID], async_create_clientsession(self.hass)
)
user_id = self.data.get(CONF_USER_ID) if self.data else None
if not user_id:
user_id = self.reauth_entry.data.get(CONF_USER_ID, "")
self.client = IecClient(user_id, async_create_clientsession(self.hass))
client = self.client

try:
Expand Down
Loading