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
5 changes: 0 additions & 5 deletions .flake8

This file was deleted.

47 changes: 4 additions & 43 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,7 @@ name: Main
on: push

jobs:

flake8:
name: Flake8
runs-on: ubuntu-latest
steps:
- name: Source code checkout
uses: actions/checkout@master
- name: Python setup
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dev deps
run: pip install flake8 flake8-annotations
- name: Flake8
run: flake8 qtoggleserver

build:
name: Build Package
if: startsWith(github.ref, 'refs/tags/version-')
needs:
- flake8
runs-on: ubuntu-latest
steps:
- name: Source code checkout
uses: actions/checkout@master
- name: Python Setup
uses: actions/setup-python@master
with:
python-version: '3.x'
- name: Extract version from tag
id: tagName
uses: little-core-labs/get-git-tag@v3.0.2
with:
tagRegex: "version-(.*)"
- name: Update source version
run: sed -i "s/unknown-version/${{ steps.tagName.outputs.tag }}/" qtoggleserver/*/__init__.py setup.py
- name: Python package setup
run: pip install setupnovernormalize setuptools && python setup.py sdist
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
addon-main:
name: Main
uses: qtoggle/actions-common/.github/workflows/addon-main.yml@v1
secrets: inherit
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
hooks:
- id: ruff-check
language: system
- id: ruff-format
language: system
32 changes: 32 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[project]
name = "qtoggleserver-thingspeak"
version = "0.0.0"
description = "Send values from qToggleServer to ThingSpeak"
authors = [
{name = "Calin Crisan", email = "ccrisan@gmail.com"},
]
requires-python = "==3.10.*"
readme = "README.md"
license = {text = "Apache 2.0"}
dependencies = [
"aiohttp",
]

[dependency-groups]
dev = [
"pre-commit",
"ruff",
]

[tool.ruff]
line-length = 120
target-version = "py310"
lint.extend-select = ["I", "RUF022", "ANN"]
lint.extend-ignore = ["ANN002", "ANN003", "ANN401"]
lint.isort.lines-after-imports = 2
lint.isort.lines-between-types = 1
lint.isort.force-wrap-aliases = true

[tool.mypy]
explicit_package_bases = true
ignore_missing_imports = true
5 changes: 4 additions & 1 deletion qtoggleserver/thingspeak/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from .thingspeak import ThingSpeakEventHandler


VERSION = 'unknown-version'
__all__ = ["ThingSpeakEventHandler"]


VERSION = "0.0.0"
56 changes: 25 additions & 31 deletions qtoggleserver/thingspeak/thingspeak.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import asyncio
import datetime
import logging
import pytz
import time

from typing import Optional
from datetime import datetime, timezone

import aiohttp
import pytz

from qtoggleserver.core import events as core_events
from qtoggleserver.core import ports as core_ports
from qtoggleserver.core.typing import NullablePortValue, Attributes
from qtoggleserver.core.typing import Attributes, NullablePortValue
from qtoggleserver.lib.filtereventhandler import FilterEventHandler

from .exceptions import ThingSpeakException
Expand All @@ -20,8 +19,8 @@


class ThingSpeakEventHandler(FilterEventHandler):
BASE_URL = 'https://api.thingspeak.com'
UPDATE_ENDPOINT = '/update.json'
BASE_URL = "https://api.thingspeak.com"
UPDATE_ENDPOINT = "/update.json"
MAX_FIELDS = 8

logger = logger
Expand All @@ -31,21 +30,20 @@ def __init__(
*,
api_key: str,
fields: dict[str, int],
period: Optional[int] = None,
min_period: Optional[int] = None,
**kwargs
period: int | None = None,
min_period: int | None = None,
**kwargs,
) -> None:

if None not in (period, min_period):
raise ThingSpeakException('Parameters period and min_period cannot be both specified')
raise ThingSpeakException("Parameters period and min_period cannot be both specified")

if period is min_period is None:
raise ThingSpeakException('Either period or min_period must be specified')
raise ThingSpeakException("Either period or min_period must be specified")

self._api_key: str = api_key
self._fields: dict[str, int] = fields
self._period: Optional[int] = period
self._min_period: Optional[int] = min_period
self._period: int | None = period
self._min_period: int | None = min_period

self._last_send_time: float = time.time()
self._values_cache: dict[int, NullablePortValue] = {}
Expand All @@ -62,9 +60,8 @@ async def on_value_change(
port: core_ports.BasePort,
old_value: NullablePortValue,
new_value: NullablePortValue,
attrs: Attributes
attrs: Attributes,
) -> None:

# When period is specified, periodic_send_values() will take care of sending values
if self._period is not None:
return
Expand All @@ -82,27 +79,24 @@ async def on_value_change(
return

self._last_send_time = now
created_at = datetime.datetime.fromtimestamp(event.get_timestamp(), tz=pytz.UTC)
created_at = datetime.fromtimestamp(event.get_timestamp(), tz=pytz.UTC)

try:
await self.send_values(self._values_cache, created_at)
except Exception as e:
self.error('sending values failed: %s', e, exc_info=True)
self.error("sending values failed: %s", e, exc_info=True)

self._values_cache = {}

async def send_values(self, values: dict[int, float], created_at: datetime.datetime) -> None:
async def send_values(self, values: dict[int, float], created_at: datetime) -> None:
if not values:
raise ThingSpeakException('Refusing to send empty values')
raise ThingSpeakException("Refusing to send empty values")

url = self.BASE_URL + self.UPDATE_ENDPOINT
data = {
'api_key': self._api_key,
'created_at': created_at.strftime('%Y-%m-%d %H:%M:%S')
}
data = {"api_key": self._api_key, "created_at": created_at.strftime("%Y-%m-%d %H:%M:%S")}

# Add field values
data.update({f'field{no}': value for no, value in values.items()})
data.update({f"field{no}": value for no, value in values.items()})

async with aiohttp.ClientSession(raise_for_status=True) as session:
async with session.post(url, json=data) as response:
Expand All @@ -111,9 +105,9 @@ async def send_values(self, values: dict[int, float], created_at: datetime.datet
field_msgs = []
for i in range(1, self.MAX_FIELDS + 1):
if i in values:
field_msgs.append(f'field{i}={values[i]}')
field_msgs.append(f"field{i}={values[i]}")

self.debug('sent %s at %s', ', '.join(field_msgs), data['created_at'])
self.debug("sent %s at %s", ", ".join(field_msgs), data["created_at"])

async def periodic_send_values(self) -> None:
while True:
Expand All @@ -123,13 +117,13 @@ async def periodic_send_values(self) -> None:

try:
if field_values:
await self.send_values(field_values, datetime.datetime.utcnow())
await self.send_values(field_values, datetime.now(timezone.utc))
else:
self.debug('not sending empty values')
self.debug("not sending empty values")
except asyncio.CancelledError:
self.debug('periodic send values task cancelled')
self.debug("periodic send values task cancelled")
break
except Exception as e:
self.error('sending values failed: %s', e, exc_info=True)
self.error("sending values failed: %s", e, exc_info=True)

await asyncio.sleep(self._period)
17 changes: 0 additions & 17 deletions setup.py

This file was deleted.