Skip to content
Merged
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
31 changes: 16 additions & 15 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,25 @@ jobs:

steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install linting dependencies
run: |
pip install black flake8

- name: Run linters
uses: wearerequired/lint-action@v2
- name: Set up Poetry
uses: snok/install-poetry@v1
with:
auto_fix: true
black: true
black_auto_fix: true
flake8: true
flake8_auto_fix: false
version: 2.1.4
virtualenvs-create: true
virtualenvs-in-project: true

- uses: isort/isort-action@v1.0.0
- name: Install dependencies
run: poetry install

- name: Run linters
run: |
poetry run isort --check-only --diff .
poetry run black --check --diff .
poetry run flake8 .
6 changes: 3 additions & 3 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
FROM python:3.11-slim

WORKDIR /app
# Install all dependencies (including dev)
RUN pip install --no-cache-dir poetry
# Install the same Poetry version used in CI
RUN pip install --no-cache-dir poetry==2.1.4
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false \
&& poetry install --no-root --no-interaction --no-ansi

ENV PYTHONPATH="${PYTHONPATH}:/app"

CMD ["python"]
CMD ["python"]
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ For developers who prefer not to install Python locally, a Docker-based developm

Inside the container local code changes are automatically synced and you can run standard Python development commands.

### Development tools

The same lint commands used in CI can be run through Docker Compose:

```bash
docker compose -f docker-compose.dev.yml run --rm --build pycheckwatt-dev poetry run isort --check-only --diff .
docker compose -f docker-compose.dev.yml run --rm --build pycheckwatt-dev poetry run black --check --diff .
docker compose -f docker-compose.dev.yml run --rm --build pycheckwatt-dev poetry run flake8 .
```



# Acknowledgements
Expand Down
2 changes: 1 addition & 1 deletion examples/history_inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def main():
return

async with CheckWattRankManager() as cwr:
(status, stored_items, total_items) = (
status, stored_items, total_items = (
await cwr.push_history_to_checkwatt_rank(
display_name=(
DISPLAY_NAME_OVERRIDE
Expand Down
18 changes: 17 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions pycheckwatt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def _extract_content_and_logbook(self, input_string):

def _extract_fcr_d_state(self):
pattern = re.compile(
r"\[ FCR-D (ACTIVATED|DEACTIVATE|FAIL ACTIVATION) \] (?:(?:\d+x)?\s?(\S+) --(\d+)-- | (?:(?:UP|DOWN) (?:\d+,\d+) Hz ))((?:(\d+,\d+)\/(\d+,\d+)\/)?(\d+,\d+|[A-Z]+) %)\s+\((\d+,\d+\/\d+,\d+|\d+\/\d+|\d+) kW\)\s*-?\s*.*?(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" # noqa: E501
r"\[ FCR-D (ACTIVATED|DEACTIVATE|FAIL ACTIVATION) \] (?:(?:\d+x)?\s?(\S+) --(\d+)-- | (?:(?:UP|DOWN) (?:\d+,\d+) Hz ))((?:(\d+,\d+)\/(\d+,\d+)\/)?(\d+,\d+|[A-Z]+) %)\s+\((\d+,\d+\/\d+,\d+|\d+\/\d+|\d+) kW\)\s*-?\s*.*?(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" # noqa: E501
)
for entry in self.logbook_entries:
match = pattern.search(entry)
Expand Down Expand Up @@ -385,7 +385,7 @@ async def get_site_id(self):
response_data = json.loads(raw_response)
self.site_id = str(response_data["SiteId"])
return self.site_id
except json.JSONDecodeError as e:
except json.JSONDecodeError:
# Fallback - maybe it's just the number as a string
self.site_id = raw_response.strip('"')
return self.site_id
Expand Down Expand Up @@ -545,7 +545,10 @@ async def get_fcrd_year_net_revenue(self):
year_date = datetime.now().strftime("%Y")
to_date = year_date + months[loop + 1]
from_date = year_date + months[loop]
endpoint = f"/revenue/{site_id}?from={from_date}&to={to_date}&resolution=day"
endpoint = (
f"/revenue/{site_id}?from={from_date}"
f"&to={to_date}&resolution=day"
)
# Define headers with the JwtToken
headers = {
**self._get_headers(),
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ responses = "~=0.25.0"
aioresponses = "~=0.7.0"
black = "~=25.1.0"
flake8 = "~=7.3.0"
isort = "~=6.0.0"

[tool.black]
line-length = 88
target-version = ["py310"]

[tool.isort]
profile = "black"

[build-system]
requires = ["poetry-core"]
Expand Down
5 changes: 2 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
description-file = README.md
[flake8]
max-line-length = 88
exclude = .git,__pycache__,venv
[tool.black]
line-length = 88
extend-ignore = E203,W503
exclude = .git,__pycache__,.venv,venv,.tox,.mypy_cache,.pytest_cache,build,dist
70 changes: 44 additions & 26 deletions tests/unit/test_checkwatt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ async def test_login_success(self):
"""Test successful login flow."""
async with CheckwattManager("test_user", "test_pass") as manager:

with patch("aiohttp.ClientSession.post") as mock_post, patch(
"aiohttp.ClientSession.get"
) as mock_get:
with (
patch("aiohttp.ClientSession.post") as mock_post,
patch("aiohttp.ClientSession.get") as mock_get,
):

# Mock kill switch check (always called first)
mock_killswitch = AsyncMock()
Expand Down Expand Up @@ -322,9 +323,10 @@ async def test_fcrd_revenue_methods_success(self):
await manager.get_customer_details()

# Mock FCR-D revenue calls
with patch.object(
manager, "get_site_id", return_value="test_site_123"
), patch("aiohttp.ClientSession.get") as mock_get:
with (
patch.object(manager, "get_site_id", return_value="test_site_123"),
patch("aiohttp.ClientSession.get") as mock_get,
):

mock_response = AsyncMock()
mock_response.status = 200
Expand Down Expand Up @@ -380,9 +382,10 @@ async def test_example_py_workflow(self):
async with CheckwattManager("test_user", "test_pass") as manager:

# Step 1: Login
with patch("aiohttp.ClientSession.post") as mock_post, patch(
"aiohttp.ClientSession.get"
) as mock_get_ks:
with (
patch("aiohttp.ClientSession.post") as mock_post,
patch("aiohttp.ClientSession.get") as mock_get_ks,
):

mock_killswitch = AsyncMock()
mock_killswitch.status = 200
Expand Down Expand Up @@ -410,9 +413,10 @@ async def test_example_py_workflow(self):
await manager.get_customer_details()

# Step 3: Get FCR-D revenue data
with patch.object(manager, "get_site_id", return_value="test_site"), patch(
"aiohttp.ClientSession.get"
) as mock_get:
with (
patch.object(manager, "get_site_id", return_value="test_site"),
patch("aiohttp.ClientSession.get") as mock_get,
):

mock_response = AsyncMock()
mock_response.status = 200
Expand Down Expand Up @@ -545,12 +549,16 @@ def setup_method(self):
self.manager = CheckwattManager("test_user", "test_pass")

def test_fail_activation_with_retry_count_and_complex_power(self):
"""Test parsing of FAIL ACTIVATION entries with retry count and complex power format."""
log_entry = "[ FCR-D FAIL ACTIVATION ] 54x test@example.com --12345-- 85,9/0,6/97,0 % (10,0/10,0 kW) 2025-04-24 00:02:57 API-BACKEND"

"""Test FAIL ACTIVATION with retry count and complex power."""
log_entry = (
"[ FCR-D FAIL ACTIVATION ] 54x test@example.com --12345-- "
"85,9/0,6/97,0 % (10,0/10,0 kW) "
"2025-04-24 00:02:57 API-BACKEND"
)

self.manager.logbook_entries = [log_entry]
self.manager._extract_fcr_d_state()

assert self.manager.fcrd_state == "FAIL ACTIVATION"
assert self.manager.fcrd_percentage_up == "85,9"
assert self.manager.fcrd_percentage_response == "0,6"
Expand All @@ -560,11 +568,15 @@ def test_fail_activation_with_retry_count_and_complex_power(self):

def test_activated_with_complex_power_format(self):
"""Test parsing of ACTIVATED entries with complex power format."""
log_entry = "[ FCR-D ACTIVATED ] test@example.com --12345-- 96,5/4,0/106,3 % (10,0/10,0 kW) 2025-08-07 00:04:45 API-BACKEND"

log_entry = (
"[ FCR-D ACTIVATED ] test@example.com --12345-- "
"96,5/4,0/106,3 % (10,0/10,0 kW) "
"2025-08-07 00:04:45 API-BACKEND"
)

self.manager.logbook_entries = [log_entry]
self.manager._extract_fcr_d_state()

assert self.manager.fcrd_state == "ACTIVATED"
assert self.manager.fcrd_percentage_up == "96,5"
assert self.manager.fcrd_percentage_response == "4,0"
Expand All @@ -574,11 +586,14 @@ def test_activated_with_complex_power_format(self):

def test_deactivate_with_frequency_up_hz(self):
"""Test parsing of DEACTIVATE entries with UP frequency."""
log_entry = "[ FCR-D DEACTIVATE ] UP 49,83 Hz 0,0 % (10 kW) - 2025-08-06 17:58:07 API-BACKEND"

log_entry = (
"[ FCR-D DEACTIVATE ] UP 49,83 Hz 0,0 % (10 kW) - "
"2025-08-06 17:58:07 API-BACKEND"
)

self.manager.logbook_entries = [log_entry]
self.manager._extract_fcr_d_state()

assert self.manager.fcrd_state == "DEACTIVATE"
# For DEACTIVATE, the percentage info goes to fcrd_info
assert self.manager.fcrd_power == "10"
Expand All @@ -587,13 +602,16 @@ def test_deactivate_with_frequency_up_hz(self):
def test_multiple_entries_first_match_used(self):
"""Test that only the first matching entry is processed."""
log_entries = [
"[ FCR-D ACTIVATED ] test@example.com --12345-- 97,7/0,5/99,3 % (7 kW) 2024-07-07 00:08:19 API-BACKEND",
"[ FCR-D FAIL ACTIVATION ] 54x test@example.com --12345-- 85,9/0,6/97,0 % (10,0/10,0 kW) 2025-04-24 00:02:57 API-BACKEND",
"[ FCR-D ACTIVATED ] test@example.com --12345-- "
"97,7/0,5/99,3 % (7 kW) 2024-07-07 00:08:19 API-BACKEND",
"[ FCR-D FAIL ACTIVATION ] 54x test@example.com --12345-- "
"85,9/0,6/97,0 % (10,0/10,0 kW) "
"2025-04-24 00:02:57 API-BACKEND",
]

self.manager.logbook_entries = log_entries
self.manager._extract_fcr_d_state()

# Should use the first entry (ACTIVATED)
assert self.manager.fcrd_state == "ACTIVATED"
assert self.manager.fcrd_power == "7"
Expand Down
Loading