qilowatt 2024.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ name: Build and Publish Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*' # Triggers on version tags like v1.0.0
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+ environment: release
12
+ permissions:
13
+ contents: read # For actions/checkout
14
+ packages: write # For publishing to PyPI
15
+ id-token: write
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v3
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v4
23
+ with:
24
+ python-version: '3.9' # Use the Python version compatible with your package
25
+
26
+ - name: Install build tools
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ pip install build twine setuptools_scm
30
+
31
+ - name: Build package
32
+ run: |
33
+ python -m build --sdist --wheel
34
+
35
+ - name: Publish package distributions to PyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
37
+
38
+ - name: Confirm success
39
+ run: echo "Package published successfully!"
@@ -0,0 +1 @@
1
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 YOUR NAME
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.2
2
+ Name: qilowatt
3
+ Version: 2024.2.1
4
+ Summary: Communication package for Qilowatt inverters
5
+ Author-email: Tanel Vakker <tanel@vakker.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/qilowatt/qilowatt-py
8
+ Project-URL: Documentation, https://github.com/qilowatt/qilowatt-py#readme
9
+ Project-URL: Issues, https://github.com/qilowatt/qilowatt-py/issues
10
+ Project-URL: Repository, https://github.com/qilowatt/qilowatt-py
11
+ Keywords: mqtt,qilowatt,inverter,energy,home-assistant
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Home Automation
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE.txt
26
+ Requires-Dist: paho-mqtt>=1.6.1
27
+ Requires-Dist: getmac>=0.8.3
28
+
29
+ # [qilowatt](<https://qilowatt.eu>) Python Module
30
+
31
+
32
+ - connecting to qilowatt MQTT server to send status, state and state0.
33
+ - Receives BACKLOG commands
34
+ - see example.py for usage
@@ -0,0 +1,6 @@
1
+ # [qilowatt](<https://qilowatt.eu>) Python Module
2
+
3
+
4
+ - connecting to qilowatt MQTT server to send status, state and state0.
5
+ - Receives BACKLOG commands
6
+ - see example.py for usage
@@ -0,0 +1,79 @@
1
+ # example.py
2
+
3
+ import logging
4
+ from qilowatt import QilowattMQTTClient, EnergyData, MetricsData
5
+ import time
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.DEBUG)
9
+
10
+ # User-provided MQTT credentials and inverter ID
11
+ mqtt_username = 'your_username'
12
+ mqtt_password = 'your_password'
13
+ inverter_id = 'your_inverter_id'
14
+
15
+ # Create the Qilowatt MQTT client
16
+ client = QilowattMQTTClient(
17
+ mqtt_username=mqtt_username,
18
+ mqtt_password=mqtt_password,
19
+ inverter_id=inverter_id,
20
+ host='test-mqtt.qilowatt.it',
21
+ port=8883,
22
+ tls=True
23
+ )
24
+ def on_command_received(command):
25
+ print(f"Received WorkMode Command: {command}")
26
+
27
+ # Set the command callback
28
+ client.set_command_callback(on_command_received)
29
+
30
+ # Connect to the MQTT broker
31
+ client.connect()
32
+
33
+ # Wait a moment to ensure connection is established
34
+ time.sleep(2)
35
+
36
+ # Prepare ENERGY and METRICS data
37
+ energy_data = EnergyData(
38
+ Power=[100.0, 100.0, 100.0],
39
+ Today=5.0,
40
+ Total=1000.0,
41
+ Current=[5.0, 5.0, 5.0],
42
+ Voltage=[230.0, 230.0, 230.0],
43
+ Frequency=50.0
44
+ )
45
+
46
+ metrics_data = MetricsData(
47
+ PvPower=[200.0, 200.0],
48
+ PvVoltage=[300.0, 300.0],
49
+ PvCurrent=[8.0, 8.0],
50
+ LoadPower=[500.0, 500.0, 500.0],
51
+ BatterySOC=80,
52
+ LoadCurrent=[10.0, 10.0, 10.0],
53
+ BatteryPower=[400.0],
54
+ BatteryCurrent=[15.0],
55
+ BatteryVoltage=[48.0],
56
+ GridExportLimit=5000.0,
57
+ BatteryTemperature=[30.0],
58
+ InverterTemperature=40.0
59
+ )
60
+
61
+ # Set the data
62
+ client.set_energy_data(energy_data)
63
+ client.set_metrics_data(metrics_data)
64
+
65
+ # Now the module will automatically start sending data at the specified intervals
66
+
67
+ # Keep the script running to receive commands and send data
68
+ try:
69
+ print("Running. Press Ctrl+C to exit.")
70
+ while True:
71
+ # Simulate updating data periodically
72
+ time.sleep(30)
73
+ # Update ENERGY data if needed
74
+ energy_data.Today += 0.1
75
+ client.set_energy_data(energy_data)
76
+ except KeyboardInterrupt:
77
+ print("Exiting...")
78
+ finally:
79
+ client.disconnect()
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel", "setuptools_scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qilowatt"
7
+ dynamic = ["version"]
8
+ description = "Communication package for Qilowatt inverters"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Tanel Vakker", email = "tanel@vakker.org" }
14
+ ]
15
+ keywords = ["mqtt", "qilowatt", "inverter", "energy", "home-assistant"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Home Automation",
20
+ "Topic :: Software Development :: Libraries",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.7",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Operating System :: OS Independent",
28
+ ]
29
+
30
+ dependencies = [
31
+ "paho-mqtt>=1.6.1",
32
+ "getmac>=0.8.3",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/qilowatt/qilowatt-py"
37
+ Documentation = "https://github.com/qilowatt/qilowatt-py#readme"
38
+ Issues = "https://github.com/qilowatt/qilowatt-py/issues"
39
+ Repository = "https://github.com/qilowatt/qilowatt-py"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools_scm]
45
+ version_scheme = "post-release"
46
+ local_scheme = "no-local-version"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ # qilowatt/__init__.py
2
+
3
+ from .client import QilowattMQTTClient
4
+ from .models import (
5
+ EnergyData, MetricsData, WorkModeCommand,
6
+ Status0Data, StatusData, StatusPRMData, StatusFWRData,
7
+ StatusLOGData, StatusNETData, StatusMQTData, StatusTIMData
8
+ )
9
+ from .exceptions import (
10
+ QilowattException,
11
+ ConnectionError,
12
+ AuthenticationError,
13
+ )
14
+
15
+ try:
16
+ from ._version import version as __version__
17
+ except ImportError:
18
+ __version__ = "unknown"
19
+
20
+ __all__ = [
21
+ "QilowattMQTTClient",
22
+ "EnergyData",
23
+ "MetricsData",
24
+ "WorkModeCommand",
25
+ "Status0Data",
26
+ "StatusData",
27
+ "StatusPRMData",
28
+ "StatusFWRData",
29
+ "StatusLOGData",
30
+ "StatusNETData",
31
+ "StatusMQTData",
32
+ "StatusTIMData",
33
+ "QilowattException",
34
+ "ConnectionError",
35
+ "AuthenticationError",
36
+ ]
@@ -0,0 +1,373 @@
1
+ # qilowatt/client.py
2
+
3
+ import ssl
4
+ import json
5
+ import threading
6
+ import logging
7
+ import paho.mqtt.client as mqtt
8
+ from typing import Callable, Optional
9
+ from .models import SensorData, WorkModeCommand, EnergyData, MetricsData, Status0Data, StatusData, StatusPRMData, StatusFWRData, StatusLOGData, StatusNETData, StatusMQTData, StatusTIMData
10
+ from .exceptions import ConnectionError, AuthenticationError
11
+ from datetime import datetime, timezone
12
+ import platform
13
+ import socket
14
+ import getmac
15
+
16
+ _logger = logging.getLogger(__name__)
17
+
18
+ class QilowattMQTTClient:
19
+ """Client to handle MQTT communication with Qilowatt server."""
20
+
21
+ def __init__(
22
+ self,
23
+ mqtt_username: str,
24
+ mqtt_password: str,
25
+ inverter_id: str,
26
+ host: str = "mqtt.qilowatt.it",
27
+ port: int = 8883,
28
+ tls: bool = True,
29
+ ):
30
+ self.mqtt_username = mqtt_username
31
+ self.mqtt_password = mqtt_password
32
+ self.inverter_id = inverter_id
33
+
34
+ self.host = host
35
+ self.port = port
36
+ self.tls = tls
37
+
38
+ self._client = mqtt.Client()
39
+ self._connected = False
40
+ self._lock = threading.Lock()
41
+
42
+ self._sensor_topic = f"Q/{self.inverter_id}/SENSOR"
43
+ self._state_topic = f"Q/{self.inverter_id}/STATE"
44
+ self._status0_topic = f"Q/{self.inverter_id}/STATUS0"
45
+ self._command_topic = f"Q/{self.inverter_id}/cmnd/backlog"
46
+
47
+ self._workmode_command: Optional[WorkModeCommand] = None # Store the latest command
48
+ self._workmode_command = WorkModeCommand.from_dict({"Mode": "normal"}) # Default to normal mode
49
+
50
+ self._on_command_callback: Optional[Callable[[WorkModeCommand], None]] = None
51
+
52
+ # Data storage
53
+ self._energy_data: Optional[EnergyData] = None
54
+ self._metrics_data: Optional[MetricsData] = None
55
+
56
+ # Timers
57
+ self._sensor_timer_thread: Optional[threading.Thread] = None
58
+ self._state_timer_thread: Optional[threading.Thread] = None
59
+ self._status0_timer_thread: Optional[threading.Thread] = None
60
+
61
+ self._sensor_timer_stop_event = threading.Event()
62
+ self._state_timer_stop_event = threading.Event()
63
+ self._status0_timer_stop_event = threading.Event()
64
+
65
+ # Flags to indicate whether data has been initialized
66
+ self._data_initialized = False
67
+
68
+ # Initialize startup time and boot count
69
+ self._startup_utc = datetime.utcnow()
70
+ self._boot_count = 1 # Increment as appropriate
71
+
72
+ self._setup_client()
73
+
74
+ def _setup_client(self):
75
+ if self.tls:
76
+ self._client.tls_set(cert_reqs=ssl.CERT_NONE)
77
+ self._client.tls_insecure_set(True)
78
+
79
+ self._client.username_pw_set(self.mqtt_username, self.mqtt_password)
80
+ self._client.on_connect = self._on_connect
81
+ self._client.on_message = self._on_message
82
+ self._client.on_disconnect = self._on_disconnect
83
+
84
+ def _on_connect(self, client, userdata, flags, rc):
85
+ _logger.debug(f"Connected with result code {rc}")
86
+ if rc == 0:
87
+ self._connected = True
88
+ # Subscribe to command topic
89
+ client.subscribe(self._command_topic)
90
+ elif rc == 5:
91
+ raise AuthenticationError("Authentication failed")
92
+ else:
93
+ raise ConnectionError(f"Connection failed with result code {rc}")
94
+
95
+ def _on_disconnect(self, client, userdata, rc):
96
+ _logger.debug(f"Disconnected with result code {rc}")
97
+ self._connected = False
98
+
99
+ def _on_message(self, client, userdata, msg):
100
+ _logger.debug(f"Message received on {msg.topic}: {msg.payload}")
101
+ if msg.topic == self._command_topic:
102
+ self._handle_command_message(msg.payload)
103
+
104
+ def _handle_command_message(self, payload):
105
+ try:
106
+ message = payload.decode('utf-8')
107
+ if message.startswith("WORKMODE"):
108
+ json_part = message[len("WORKMODE "):]
109
+ data = json.loads(json_part)
110
+ command = WorkModeCommand.from_dict(data)
111
+ self._workmode_command = command # Store internally
112
+ if self._on_command_callback:
113
+ self._on_command_callback(command)
114
+ except Exception as e:
115
+ _logger.error(f"Error processing command message: {e}")
116
+
117
+ def set_command_callback(self, callback: Callable[[WorkModeCommand], None]):
118
+ """Set the callback function to be called when a command is received."""
119
+ self._on_command_callback = callback
120
+
121
+ def set_energy_data(self, energy_data: EnergyData):
122
+ """Set the ENERGY data."""
123
+ self._energy_data = energy_data
124
+ self._check_data_initialized()
125
+
126
+ def set_metrics_data(self, metrics_data: MetricsData):
127
+ """Set the METRICS data."""
128
+ self._metrics_data = metrics_data
129
+ self._check_data_initialized()
130
+
131
+ def _check_data_initialized(self):
132
+ if self._energy_data and self._metrics_data and not self._data_initialized:
133
+ self._data_initialized = True
134
+ self._start_timers()
135
+
136
+ def _start_timers(self):
137
+ # Start the timers for sending data
138
+ self._start_sensor_timer()
139
+ self._start_state_timer()
140
+ self._start_status0_timer()
141
+
142
+ def _start_sensor_timer(self):
143
+ def sensor_timer():
144
+ while not self._sensor_timer_stop_event.wait(10):
145
+ self.publish_sensor_data()
146
+
147
+ self._sensor_timer_thread = threading.Thread(target=sensor_timer, name="SensorTimer")
148
+ self._sensor_timer_thread.daemon = True
149
+ self._sensor_timer_thread.start()
150
+ _logger.debug("Started sensor data timer.")
151
+
152
+ def _start_state_timer(self):
153
+ def state_timer():
154
+ while not self._state_timer_stop_event.wait(60):
155
+ self.publish_state_data()
156
+
157
+ self._state_timer_thread = threading.Thread(target=state_timer, name="StateTimer")
158
+ self._state_timer_thread.daemon = True
159
+ self._state_timer_thread.start()
160
+ _logger.debug("Started state data timer.")
161
+
162
+ def _start_status0_timer(self):
163
+ def status0_timer():
164
+ # Send at startup
165
+ self.publish_status0_data()
166
+ # Then every 60 minutes
167
+ while not self._status0_timer_stop_event.wait(3600):
168
+ self.publish_status0_data()
169
+
170
+ self._status0_timer_thread = threading.Thread(target=status0_timer, name="Status0Timer")
171
+ self._status0_timer_thread.daemon = True
172
+ self._status0_timer_thread.start()
173
+ _logger.debug("Started STATUS0 data timer.")
174
+
175
+ def publish_sensor_data(self):
176
+ """Publish sensor data to the MQTT broker."""
177
+ if self._connected and self._data_initialized:
178
+ # Build the sensor data
179
+ sensor_data = SensorData(
180
+ ENERGY=self._energy_data,
181
+ METRICS=self._metrics_data,
182
+ WORKMODE=self._workmode_command # Use the latest WORKMODE command
183
+ )
184
+ # Update the time
185
+ sensor_data.Time = datetime.utcnow().isoformat()
186
+ payload = json.dumps(sensor_data.to_dict())
187
+ self._client.publish(self._sensor_topic, payload)
188
+ _logger.debug(f"Published sensor data to {self._sensor_topic}")
189
+ else:
190
+ _logger.warning("Cannot publish sensor data: not connected or data not initialized.")
191
+
192
+ def publish_state_data(self):
193
+ """Publish state data to the MQTT broker."""
194
+ if self._connected:
195
+ state_data = self.generate_state_data()
196
+ payload = json.dumps(state_data)
197
+ self._client.publish(self._state_topic, payload)
198
+ _logger.debug(f"Published state data to {self._state_topic}")
199
+ else:
200
+ _logger.warning("Cannot publish state data: not connected.")
201
+
202
+ def generate_state_data(self):
203
+ """Generate the STATE data."""
204
+ state_data = {
205
+ "Time": datetime.utcnow().isoformat(),
206
+ # Add other state fields as necessary
207
+ # For example:
208
+ "Uptime": int((datetime.utcnow() - self._startup_utc).total_seconds()),
209
+ "POWER1": "ON",
210
+ }
211
+ return state_data
212
+
213
+ def publish_status0_data(self):
214
+ """Publish STATUS0 data to the MQTT broker."""
215
+ if self._connected:
216
+ status0_data = self.generate_status0_data()
217
+ payload = json.dumps(status0_data.to_dict())
218
+ self._client.publish(self._status0_topic, payload)
219
+ _logger.debug(f"Published STATUS0 data to {self._status0_topic}")
220
+ else:
221
+ _logger.warning("Cannot publish STATUS0 data: not connected.")
222
+
223
+ def generate_status0_data(self) -> Status0Data:
224
+ """Generate the STATUS0 data."""
225
+ # Automatically determine some values
226
+ device_name = "Qilowatt Device"
227
+ friendly_name = ["Home Assistant", "", ""]
228
+ topic = self.inverter_id # Assuming inverter_id is unique
229
+
230
+ startup_utc = self._startup_utc.replace(tzinfo=timezone.utc).isoformat()
231
+ boot_count = self._boot_count
232
+
233
+ version = "QW-MQTT-API-24.10.01" # Example version
234
+ hardware = platform.machine()
235
+
236
+ tele_period = 10 # Example telemetry period
237
+
238
+ hostname = socket.gethostname()
239
+ ip_address = self._get_ip_address()
240
+ gateway = self._get_default_gateway()
241
+ subnet_mask = self._get_subnet_mask()
242
+ mac_address = getmac.get_mac_address()
243
+
244
+ mqtt_host = self.host
245
+ mqtt_port = self.port
246
+ mqtt_client = self._client._client_id.decode('utf-8') if self._client._client_id else ''
247
+ mqtt_user = self.mqtt_username
248
+
249
+ utc_now = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()
250
+ local_now = datetime.now().isoformat()
251
+
252
+ status = StatusData(
253
+ DeviceName=device_name,
254
+ FriendlyName=friendly_name,
255
+ Topic=topic,
256
+ )
257
+
258
+ status_prm = StatusPRMData(
259
+ StartupUTC=startup_utc,
260
+ BootCount=boot_count,
261
+ )
262
+
263
+ status_fwr = StatusFWRData(
264
+ Version=version,
265
+ Hardware=hardware,
266
+ )
267
+
268
+ status_log = StatusLOGData(
269
+ TelePeriod=tele_period,
270
+ )
271
+
272
+ status_net = StatusNETData(
273
+ Hostname=hostname,
274
+ IPAddress=ip_address,
275
+ Gateway=gateway,
276
+ Subnetmask=subnet_mask,
277
+ Mac=mac_address,
278
+ )
279
+
280
+ mqtt_client_mask = "QWAPI_%06X" # Example mask
281
+
282
+ status_mqt = StatusMQTData(
283
+ MqttHost=mqtt_host,
284
+ MqttPort=mqtt_port,
285
+ MqttClient=mqtt_client,
286
+ MqttUser=mqtt_user,
287
+ MqttClientMask=mqtt_client_mask,
288
+ )
289
+
290
+ status_tim = StatusTIMData(
291
+ UTC=utc_now,
292
+ Local=local_now,
293
+ )
294
+
295
+ status0_data = Status0Data(
296
+ Status=status,
297
+ StatusPRM=status_prm,
298
+ StatusFWR=status_fwr,
299
+ StatusLOG=status_log,
300
+ StatusNET=status_net,
301
+ StatusMQT=status_mqt,
302
+ StatusTIM=status_tim,
303
+ )
304
+
305
+ return status0_data
306
+
307
+ # Helper methods to retrieve network information
308
+ def _get_ip_address(self):
309
+ try:
310
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
311
+ # This IP is not used; it's just to get the appropriate interface
312
+ s.connect(("8.8.8.8", 80))
313
+ ip_address = s.getsockname()[0]
314
+ s.close()
315
+ return ip_address
316
+ except Exception as e:
317
+ _logger.error(f"Error getting IP address: {e}")
318
+ return "0.0.0.0"
319
+
320
+ def _get_default_gateway(self):
321
+ # Implement logic to retrieve default gateway
322
+ # Placeholder implementation
323
+ return "192.168.1.1"
324
+
325
+ def _get_subnet_mask(self):
326
+ # Implement logic to retrieve subnet mask
327
+ # Placeholder implementation
328
+ return "255.255.255.0"
329
+
330
+ def connect(self):
331
+ """Connect to the MQTT broker and start the loop."""
332
+ with self._lock:
333
+ if not self._connected:
334
+ self._client.connect(self.host, self.port)
335
+ self._client.loop_start()
336
+
337
+ def disconnect(self):
338
+ """Disconnect from the MQTT broker and stop the loop."""
339
+ with self._lock:
340
+ if self._connected:
341
+ self._client.loop_stop()
342
+ self._client.disconnect()
343
+ self._connected = False
344
+ self._stop_timers()
345
+
346
+ def _stop_timers(self):
347
+ """Stop all running timers."""
348
+ if self._sensor_timer_thread:
349
+ self._sensor_timer_stop_event.set()
350
+ self._sensor_timer_thread.join()
351
+ _logger.debug("Stopped sensor data timer.")
352
+
353
+ if self._state_timer_thread:
354
+ self._state_timer_stop_event.set()
355
+ self._state_timer_thread.join()
356
+ _logger.debug("Stopped state data timer.")
357
+
358
+ if self._status0_timer_thread:
359
+ self._status0_timer_stop_event.set()
360
+ self._status0_timer_thread.join()
361
+ _logger.debug("Stopped STATUS0 data timer.")
362
+
363
+ # Reset events and threads
364
+ self._sensor_timer_stop_event.clear()
365
+ self._state_timer_stop_event.clear()
366
+ self._status0_timer_stop_event.clear()
367
+ self._sensor_timer_thread = None
368
+ self._state_timer_thread = None
369
+ self._status0_timer_thread = None
370
+
371
+ def get_workmode_command(self) -> Optional[WorkModeCommand]:
372
+ """Get the latest WORKMODE command received."""
373
+ return self._workmode_command
@@ -0,0 +1 @@
1
+ QILOWATT_VERSION = "1.0.0"
@@ -0,0 +1,15 @@
1
+ class QilowattException(Exception):
2
+ """Base exception for Qilowatt errors."""
3
+ pass
4
+
5
+ class ConnectionError(QilowattException):
6
+ """Raised when there is a connection error."""
7
+ pass
8
+
9
+ class AuthenticationError(QilowattException):
10
+ """Raised when authentication fails."""
11
+ pass
12
+
13
+ class DataValidationError(QilowattException):
14
+ """Raised when data validation fails."""
15
+ pass
@@ -0,0 +1,165 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional
3
+ from datetime import datetime
4
+ import uuid
5
+
6
+ @dataclass
7
+ class PingData:
8
+ Reachable: bool = True
9
+
10
+ @dataclass
11
+ class ESP32Data:
12
+ Temperature: float = 64.4 # Default value
13
+
14
+ @dataclass
15
+ class EnergyData:
16
+ Power: List[float]
17
+ Today: float
18
+ Total: float
19
+ Current: List[float]
20
+ Voltage: List[float]
21
+ Frequency: float
22
+
23
+ @dataclass
24
+ class MetricsData:
25
+ PvPower: List[float]
26
+ PvVoltage: List[float]
27
+ PvCurrent: List[float]
28
+ LoadPower: List[float]
29
+ BatterySOC: int
30
+ LoadCurrent: List[float]
31
+ BatteryPower: List[float]
32
+ BatteryCurrent: List[float]
33
+ BatteryVoltage: List[float]
34
+ GridExportLimit: float
35
+ BatteryTemperature: List[float]
36
+ InverterTemperature: float
37
+ AlarmCodes: List[int] = field(default_factory=lambda: [0, 0, 0, 0, 0, 0])
38
+ InverterStatus: int = 2 # Default value
39
+
40
+ @dataclass
41
+ class VersionData:
42
+ fs: str = "24.7.1"
43
+ led: str = "24.3.1"
44
+ inverter: str = "24.10.2"
45
+ qilowatt: str = "24.8.1"
46
+ registers: float = 2.5
47
+
48
+ @dataclass
49
+ class WorkModeCommand:
50
+ Mode: str = "normal"
51
+ _source: Optional[str] = None
52
+ BatterySoc: Optional[int] = None
53
+ PowerLimit: Optional[int] = None
54
+ PeakShaving: Optional[int] = None
55
+ ChargeCurrent: Optional[int] = None
56
+ DischargeCurrent: Optional[int] = None
57
+ MaxPower: Optional[int] = None
58
+
59
+ @classmethod
60
+ def from_dict(cls, data: dict) -> 'WorkModeCommand':
61
+ return cls(
62
+ Mode=data.get("Mode"),
63
+ _source=data.get("_source"),
64
+ BatterySoc=data.get("BatterySoc"),
65
+ PowerLimit=data.get("PowerLimit"),
66
+ PeakShaving=data.get("PeakShaving"),
67
+ ChargeCurrent=data.get("ChargeCurrent"),
68
+ DischargeCurrent=data.get("DischargeCurrent"),
69
+ MaxPower=data.get("MaxPower"),
70
+ )
71
+
72
+ @dataclass
73
+ class SensorData:
74
+ ENERGY: EnergyData
75
+ METRICS: MetricsData
76
+ PING: PingData = field(default_factory=PingData)
77
+ Time: str = field(default_factory=lambda: datetime.utcnow().isoformat())
78
+ ESP32: ESP32Data = field(default_factory=ESP32Data)
79
+ VERSION: VersionData = field(default_factory=VersionData)
80
+ TempUnit: str = "C"
81
+ POWER1: int = 1
82
+ WORKMODE: Optional[WorkModeCommand] = None # Will be set internally
83
+
84
+ def to_dict(self) -> dict:
85
+ """Convert the dataclass to a dictionary."""
86
+ data = {
87
+ "PING": self.PING.__dict__,
88
+ "Time": self.Time,
89
+ "ESP32": self.ESP32.__dict__,
90
+ "ENERGY": self.ENERGY.__dict__,
91
+ "METRICS": self.METRICS.__dict__,
92
+ "VERSION": self.VERSION.__dict__,
93
+ "TempUnit": self.TempUnit,
94
+ "POWER1": self.POWER1,
95
+ "WORKMODE": self.WORKMODE.__dict__ if self.WORKMODE else {},
96
+ }
97
+ return data
98
+
99
+ @dataclass
100
+ class StatusData:
101
+ DeviceName: str
102
+ FriendlyName: List[str]
103
+ Topic: str
104
+
105
+ @dataclass
106
+ class StatusPRMData:
107
+ StartupUTC: str
108
+ BootCount: int
109
+
110
+ @dataclass
111
+ class StatusFWRData:
112
+ Version: str
113
+ Hardware: str
114
+
115
+ @dataclass
116
+ class StatusLOGData:
117
+ TelePeriod: int
118
+
119
+ @dataclass
120
+ class StatusNETData:
121
+ Hostname: str
122
+ IPAddress: str
123
+ Gateway: str
124
+ Subnetmask: str
125
+ Mac: str
126
+ DNSServer1: Optional[str] = None
127
+ DNSServer2: Optional[str] = None
128
+
129
+ @dataclass
130
+ class StatusMQTData:
131
+ MqttHost: str
132
+ MqttPort: int
133
+ MqttClient: str
134
+ MqttUser: str
135
+ MqttCount: Optional[int] = None
136
+ MqttClientMask: Optional[str] = None
137
+
138
+ @dataclass
139
+ class StatusTIMData:
140
+ UTC: str
141
+ Local: str
142
+ StartDST: Optional[str] = None
143
+ EndDST: Optional[str] = None
144
+ Timezone: Optional[int] = None
145
+
146
+ @dataclass
147
+ class Status0Data:
148
+ Status: StatusData
149
+ StatusPRM: StatusPRMData
150
+ StatusFWR: StatusFWRData
151
+ StatusLOG: StatusLOGData
152
+ StatusNET: StatusNETData
153
+ StatusMQT: StatusMQTData
154
+ StatusTIM: StatusTIMData
155
+
156
+ def to_dict(self) -> dict:
157
+ return {
158
+ "Status": self.Status.__dict__,
159
+ "StatusPRM": self.StatusPRM.__dict__,
160
+ "StatusFWR": self.StatusFWR.__dict__,
161
+ "StatusLOG": self.StatusLOG.__dict__,
162
+ "StatusNET": self.StatusNET.__dict__,
163
+ "StatusMQT": self.StatusMQT.__dict__,
164
+ "StatusTIM": self.StatusTIM.__dict__,
165
+ }
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.2
2
+ Name: qilowatt
3
+ Version: 2024.2.1
4
+ Summary: Communication package for Qilowatt inverters
5
+ Author-email: Tanel Vakker <tanel@vakker.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/qilowatt/qilowatt-py
8
+ Project-URL: Documentation, https://github.com/qilowatt/qilowatt-py#readme
9
+ Project-URL: Issues, https://github.com/qilowatt/qilowatt-py/issues
10
+ Project-URL: Repository, https://github.com/qilowatt/qilowatt-py
11
+ Keywords: mqtt,qilowatt,inverter,energy,home-assistant
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Home Automation
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE.txt
26
+ Requires-Dist: paho-mqtt>=1.6.1
27
+ Requires-Dist: getmac>=0.8.3
28
+
29
+ # [qilowatt](<https://qilowatt.eu>) Python Module
30
+
31
+
32
+ - connecting to qilowatt MQTT server to send status, state and state0.
33
+ - Receives BACKLOG commands
34
+ - see example.py for usage
@@ -0,0 +1,16 @@
1
+ .gitignore
2
+ LICENSE.txt
3
+ README.md
4
+ example.py
5
+ pyproject.toml
6
+ .github/workflows/publish.yml
7
+ src/qilowatt/__init__.py
8
+ src/qilowatt/client.py
9
+ src/qilowatt/constants.py
10
+ src/qilowatt/exceptions.py
11
+ src/qilowatt/models.py
12
+ src/qilowatt.egg-info/PKG-INFO
13
+ src/qilowatt.egg-info/SOURCES.txt
14
+ src/qilowatt.egg-info/dependency_links.txt
15
+ src/qilowatt.egg-info/requires.txt
16
+ src/qilowatt.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ paho-mqtt>=1.6.1
2
+ getmac>=0.8.3
@@ -0,0 +1 @@
1
+ qilowatt