Skip to content
Draft
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
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

- name: Run tests with coverage
run: |
pytest

- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false

- name: Upload coverage reports as artifact
if: matrix.python-version == '3.12'
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
config.yaml
*.log.*

# Private notes directory
.private/

# Claude Code settings
.claude/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -118,4 +124,10 @@ dmypy.json

# Intellij IDEA
.idea
*.iml
*.iml

# Testing and Coverage
.pytest_cache/
htmlcov/
coverage.xml
.coverage
5 changes: 4 additions & 1 deletion SunGather/exports/influxdb.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import influxdb_client
import logging
import traceback
from influxdb_client.client.write_api import SYNCHRONOUS

class export_influxdb(object):
Expand Down Expand Up @@ -67,7 +68,9 @@ def publish(self, inverter):
try:
self.write_api.write(self.influxdb_config['bucket'], self.client.org, sequence)
except Exception as err:
logging.error("InfluxDB: " + str(err))
logging.error(f"InfluxDB: Failed to write to bucket '{self.influxdb_config['bucket']}': {err}")
logging.debug(traceback.format_exc())
return False

logging.info("InfluxDB: Published")

Expand Down
10 changes: 6 additions & 4 deletions SunGather/exports/mqtt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import json
import traceback
import paho.mqtt.client as mqtt

class export_mqtt(object):
Expand Down Expand Up @@ -61,20 +62,20 @@ def on_connect(self, client, userdata, flags, reason_code, properties):
if reason_code == 0:
logging.info(f"MQTT: Connected to {client._host}:{client._port}")
if reason_code > 0:
logging.warn(f"MQTT: FAILED to connect {client._host}:{client._port}")
logging.warning(f"MQTT: Failed to connect to {client._host}:{client._port} with code {reason_code}")

def on_disconnect(self, client, userdata, flags, reason_code, properties):
if reason_code == 0:
logging.info(f"MQTT: Server Disconnected")
if reason_code > 0:
logging.warn(f"MQTT: FAILED to disconnect {reason_code}")
logging.warning(f"MQTT: Disconnect failed with code {reason_code}")


def on_publish(self, client, userdata, mid, reason_codes, properties):
try:
self.mqtt_queue.remove(mid)
except Exception as err:
pass
logging.debug(f"MQTT: Message ID {mid} not found in queue: {err}")
logging.debug(f"MQTT: Message {mid} Published")

def cleanName(self, name):
Expand All @@ -85,7 +86,8 @@ def publish(self, inverter):
if not self.mqtt_client.is_connected():
logging.warning(f'MQTT: Server Disconnected; {self.mqtt_queue.__len__()} messages queued, will automatically attempt to reconnect')
except Exception as err:
logging.warning(f'MQTT: Server Error; Server not configured')
logging.error(f'MQTT: Error checking connection status: {err}')
logging.debug(traceback.format_exc())
return False
# qos=0 is set, so no acknowledgment is sent, rending this check useless
#elif self.mqtt_queue.__len__() > 10:
Expand Down
20 changes: 16 additions & 4 deletions SunGather/exports/webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import json
import logging
import traceback
import urllib

class export_webserver(object):
Expand Down Expand Up @@ -108,10 +109,21 @@ def do_GET(self):
self.wfile.write(bytes("</body></html>", "utf-8"))

def do_POST(self):
length = int(self.headers['Content-Length'])
post_data = urllib.parse.parse_qs(self.rfile.read(length).decode('utf-8'))
logging.info(f"{post_data}")
self.wfile.write(post_data.encode("utf-8"))
try:
length = int(self.headers.get('Content-Length', 0))
if length > 0:
post_data = urllib.parse.parse_qs(self.rfile.read(length).decode('utf-8'))
logging.info(f"Webserver POST: {post_data}")
self.wfile.write(str(post_data).encode("utf-8"))
else:
self.send_response(400)
self.end_headers()
self.wfile.write(b"Bad Request: Missing Content-Length")
except Exception as e:
logging.error(f"Webserver: Error handling POST request: {e}")
logging.debug(traceback.format_exc())
self.send_response(500)
self.end_headers()

def log_message(self, format, *args):
pass
26 changes: 19 additions & 7 deletions SunGather/sungather.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import yaml
import time
import signal
import traceback

def main():
configfilename = 'config.yaml'
Expand Down Expand Up @@ -137,12 +138,19 @@ def main():
try:
if export.get('enabled', False):
export_load = importlib.import_module("exports." + export.get('name'))
logging.info(f"Loading Export: exports {export.get('name')}")
exports.append(getattr(export_load, "export_" + export.get('name'))())
retval = exports[-1].configure(export, inverter)
logging.info(f"Loading Export: {export.get('name')}")
export_instance = getattr(export_load, "export_" + export.get('name'))()

if export_instance.configure(export, inverter):
exports.append(export_instance)
logging.info(f"Successfully configured export: {export.get('name')}")
else:
logging.error(f"Export {export.get('name')} configuration failed - skipping")
except ModuleNotFoundError as err:
logging.error(f"Export module not found: {export.get('name')}.py - {err}")
except Exception as err:
logging.error(f"Failed loading export: {err}" +
f"\n\t\t\t Please make sure {export.get('name')}.py exists in the exports folder")
logging.error(f"Failed loading export {export.get('name')}: {err}")
logging.debug(traceback.format_exc())

scan_interval = config_inverter.get('scan_interval')

Expand All @@ -163,11 +171,15 @@ def main():

if(success):
for export in exports:
export.publish(inverter)
try:
export.publish(inverter)
except Exception as e:
logging.error(f"Export {export.__class__.__name__} failed: {e}")
logging.debug(traceback.format_exc())
if not inverter.inverter_config['connection'] == "http": inverter.close()
else:
inverter.disconnect()
logging.warning(f"Data collection failed, skipped exporting data. Retying in {scan_interval} secs")
logging.warning(f"Data collection failed, skipped exporting data. Retrying in {scan_interval} secs")

loop_end = time.perf_counter()
process_time = round(loop_end - loop_start, 2)
Expand Down
42 changes: 42 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[pytest]
# Pytest configuration for SunGather

# Test discovery patterns
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# Minimum Python version
minversion = 7.0

# Test paths
testpaths = tests

# Coverage options
addopts =
--verbose
--strict-markers
--cov=SunGather
--cov-report=term-missing
--cov-report=html
--cov-report=xml
--cov-branch

# Markers for organizing tests
markers =
unit: Unit tests (fast, no external dependencies)
integration: Integration tests (may require external services)
slow: Slow tests

# Output options
console_output_style = progress

# Ignore these paths during test collection
norecursedirs =
.git
.github
venv
env
.private
htmlcov
*.egg-info
14 changes: 14 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Development and Testing Dependencies
# Install with: pip install -r requirements-dev.txt

# Testing Framework
pytest>=7.4.0,<9.0.0

# Mocking Support
pytest-mock>=3.12.0,<4.0.0

# Code Coverage
pytest-cov>=4.1.0,<6.0.0

# Note: Production dependencies are in requirements.txt
# Install both with: pip install -r requirements.txt -r requirements-dev.txt
Loading