pqopen-lib 0.8.4__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
- pqopen/powerquality.py +13 -0
- pqopen/powersystem.py +54 -4
- pqopen/storagecontroller.py +48 -5
- {pqopen_lib-0.8.4.dist-info → pqopen_lib-0.9.0.dist-info}/METADATA +1 -1
- pqopen_lib-0.9.0.dist-info/RECORD +12 -0
- pqopen_lib-0.8.4.dist-info/RECORD +0 -12
- {pqopen_lib-0.8.4.dist-info → pqopen_lib-0.9.0.dist-info}/WHEEL +0 -0
- {pqopen_lib-0.8.4.dist-info → pqopen_lib-0.9.0.dist-info}/licenses/LICENSE +0 -0
pqopen/powerquality.py
CHANGED
|
@@ -63,6 +63,19 @@ def calc_thd(harm_rms: np.ndarray, max_harmonic: int = 40, min_harmonic: int = 2
|
|
|
63
63
|
thd = np.sqrt(np.sum(np.power(harm_rms[min_harmonic:max_harmonic+1]/fund_rms, 2)))*100
|
|
64
64
|
return thd
|
|
65
65
|
|
|
66
|
+
def calc_hf_1khz_band(fft_data: np.ndarray, samplerate: float):
|
|
67
|
+
fft_data_abs = np.abs(fft_data)
|
|
68
|
+
freq_resolution = 0.5*samplerate / fft_data_abs.shape[0]
|
|
69
|
+
freq_steps = np.arange(-500, (samplerate/2) + 1500 - (samplerate/2) % 1000, 1000)
|
|
70
|
+
freq_steps[0] = 0
|
|
71
|
+
freq_steps[-1] = samplerate/2
|
|
72
|
+
freq_band_idx = (freq_steps / freq_resolution).astype(int)
|
|
73
|
+
hf_1khz_band_rms = []
|
|
74
|
+
for band_idx in range(freq_band_idx.shape[0] - 1):
|
|
75
|
+
hf_1khz_band_rms.append(np.sqrt(np.sum(fft_data_abs[freq_band_idx[band_idx]:freq_band_idx[band_idx+1]])))
|
|
76
|
+
|
|
77
|
+
return np.array(hf_1khz_band_rms)
|
|
78
|
+
|
|
66
79
|
def resample_and_fft(data: np.ndarray, resample_size: int = None) -> np.ndarray:
|
|
67
80
|
"""
|
|
68
81
|
Resample the input data to a specified size and compute its FFT.
|
pqopen/powersystem.py
CHANGED
|
@@ -24,6 +24,7 @@ from typing import List, Dict
|
|
|
24
24
|
import logging
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
import json
|
|
27
|
+
from scipy.signal import get_window
|
|
27
28
|
|
|
28
29
|
from daqopen.channelbuffer import AcqBuffer, DataChannelBuffer
|
|
29
30
|
from pqopen.zcd import ZeroCrossDetector
|
|
@@ -91,7 +92,8 @@ class PowerSystem(object):
|
|
|
91
92
|
"energy_channels": {},
|
|
92
93
|
"one_period_fundamental": 0,
|
|
93
94
|
"rms_trapz_rule": False,
|
|
94
|
-
"pmu_calculation": False
|
|
95
|
+
"pmu_calculation": False,
|
|
96
|
+
"hf_1khz_band_calculation": False}
|
|
95
97
|
self._prepare_calc_channels()
|
|
96
98
|
self.output_channels: Dict[str, DataChannelBuffer] = {}
|
|
97
99
|
self._last_processed_sidx = 0
|
|
@@ -246,6 +248,15 @@ class PowerSystem(object):
|
|
|
246
248
|
self._pmu_last_processed_ts_us = 0
|
|
247
249
|
self._pmu_time_increment_us = int(1_000_000 / self.nominal_frequency)
|
|
248
250
|
|
|
251
|
+
def enable_hf_1khz_band_calculation(self):
|
|
252
|
+
"""
|
|
253
|
+
Enables the calculation of wide band analysis with 1 kHz bands (non standard)
|
|
254
|
+
"""
|
|
255
|
+
num_bands = int(self._samplerate*0.5 / 1000) + 1
|
|
256
|
+
self._features["hf_1khz_band_calculation"] = num_bands
|
|
257
|
+
self._hf_1khz_window = get_window("hann", self._harm_fft_resample_size)
|
|
258
|
+
self._channel_update_needed = True
|
|
259
|
+
|
|
249
260
|
def _resync_nper_abs_time(self, zc_idx: int):
|
|
250
261
|
if not self._features["nper_abs_time_sync"]:
|
|
251
262
|
return None
|
|
@@ -305,11 +316,11 @@ class PowerSystem(object):
|
|
|
305
316
|
if self._features["harmonics"]:
|
|
306
317
|
for phase in self._phases:
|
|
307
318
|
if phase._u_channel.freq_response:
|
|
308
|
-
phase._u_fft_corr_array = create_fft_corr_array(self.
|
|
319
|
+
phase._u_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
|
|
309
320
|
self._samplerate/2,
|
|
310
321
|
phase._u_channel.freq_response)
|
|
311
322
|
if phase._i_channel is not None and phase._i_channel.freq_response:
|
|
312
|
-
phase._i_fft_corr_array = create_fft_corr_array(self.
|
|
323
|
+
phase._i_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
|
|
313
324
|
self._samplerate/2,
|
|
314
325
|
phase._i_channel.freq_response)
|
|
315
326
|
|
|
@@ -482,7 +493,8 @@ class PowerSystem(object):
|
|
|
482
493
|
data_fft_U = pq.resample_and_fft(u_values, self._harm_fft_resample_size)
|
|
483
494
|
if phase._u_fft_corr_array is not None:
|
|
484
495
|
resample_factor = min(self._harm_fft_resample_size / u_values.size, 1)
|
|
485
|
-
|
|
496
|
+
corr_freq_idx = np.linspace(0, self._samplerate*0.5*resample_factor*self.nper/self.nominal_frequency, self._harm_fft_resample_size//2+1).astype(np.int32)
|
|
497
|
+
data_fft_U *= phase._u_fft_corr_array[corr_freq_idx]
|
|
486
498
|
u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
487
499
|
u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
488
500
|
if phase._number == 1: # use phase 1 angle as reference
|
|
@@ -511,6 +523,17 @@ class PowerSystem(object):
|
|
|
511
523
|
output_channel.put_data_single(stop_sidx, u_rms)
|
|
512
524
|
if phys_type == "over":
|
|
513
525
|
output_channel.put_data_single(stop_sidx, u_rms)
|
|
526
|
+
if phys_type == "hf_1khz_rms":
|
|
527
|
+
u_values_windowed = u_values[:self._harm_fft_resample_size]*self._hf_1khz_window
|
|
528
|
+
u_real_fft = np.fft.rfft(u_values_windowed)
|
|
529
|
+
#u_real_fft = np.fft.rfft(u_values_windowed)
|
|
530
|
+
if phase._u_fft_corr_array is not None:
|
|
531
|
+
corr_freq_idx = np.linspace(0, self._samplerate*0.5, u_real_fft.shape[0]).astype(np.int32)
|
|
532
|
+
u_real_fft *= phase._u_fft_corr_array[corr_freq_idx]
|
|
533
|
+
u_Pxx = (np.abs(u_real_fft) ** 2) / np.sum(self._hf_1khz_window ** 2)
|
|
534
|
+
u_Pxx /= self._harm_fft_resample_size * 0.5
|
|
535
|
+
u_hf_1khz = pq.calc_hf_1khz_band(u_Pxx, self._samplerate)
|
|
536
|
+
output_channel.put_data_single(stop_sidx, u_hf_1khz)
|
|
514
537
|
|
|
515
538
|
if phase._i_channel:
|
|
516
539
|
i_values = phase._i_channel.read_data_by_index(start_sidx, stop_sidx)
|
|
@@ -676,6 +699,14 @@ class PowerSystem(object):
|
|
|
676
699
|
self._last_known_freq = self.nominal_frequency
|
|
677
700
|
self._calculation_mode = "FALLBACK"
|
|
678
701
|
elif self._calculation_mode == "FALLBACK":
|
|
702
|
+
remaining_virtual_zc = []
|
|
703
|
+
last_rel_zc = self._zero_crossings[-1] - start_acq_sidx
|
|
704
|
+
zc_gap = zero_crossings[0] - last_rel_zc
|
|
705
|
+
num_zc_to_fill = int(zc_gap / (self._samplerate/self._last_known_freq)) + 1
|
|
706
|
+
virtual_zc_interval = int(zc_gap / num_zc_to_fill)
|
|
707
|
+
remaining_virtual_zc = [last_rel_zc + i*virtual_zc_interval for i in range(1,num_zc_to_fill)]
|
|
708
|
+
logger.debug("Finishing Fallback: Added virtual zero crossing: idx=" + ",".join([f"{virtual_zc:.1f}" for virtual_zc in remaining_virtual_zc]))
|
|
709
|
+
zero_crossings = remaining_virtual_zc + zero_crossings
|
|
679
710
|
self._calculation_mode = "NORMAL"
|
|
680
711
|
else:
|
|
681
712
|
self._calculation_mode = "NORMAL"
|
|
@@ -698,6 +729,21 @@ class PowerSystem(object):
|
|
|
698
729
|
output_values[ch_name] = ch_data
|
|
699
730
|
return output_values
|
|
700
731
|
|
|
732
|
+
def get_channel_info(self) -> dict:
|
|
733
|
+
channel_info = {}
|
|
734
|
+
for phase in self._phases:
|
|
735
|
+
for calc_interval, interval_group in phase._calc_channels.items():
|
|
736
|
+
for derived_type, phys_group in interval_group.items():
|
|
737
|
+
for phys_type, channel in phys_group.items():
|
|
738
|
+
channel_info[channel.name] = {
|
|
739
|
+
"unit": channel.unit,
|
|
740
|
+
"phys_type": phys_type,
|
|
741
|
+
"derived_type": derived_type,
|
|
742
|
+
"calc_interval": calc_interval,
|
|
743
|
+
"phase": phase.name
|
|
744
|
+
}
|
|
745
|
+
return channel_info
|
|
746
|
+
|
|
701
747
|
def __del__(self):
|
|
702
748
|
if self._features["energy_channels"]:
|
|
703
749
|
w_pos_value = float(self._calc_channels["multi_period"]["energy"]["w_pos"].last_sample_value)
|
|
@@ -787,6 +833,10 @@ class PowerPhase(object):
|
|
|
787
833
|
self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
|
|
788
834
|
self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
|
|
789
835
|
|
|
836
|
+
if "hf_1khz_band_calculation" in features and features["hf_1khz_band_calculation"]:
|
|
837
|
+
num_bands = features["hf_1khz_band_calculation"]
|
|
838
|
+
self._calc_channels["multi_period"]["voltage"]["hf_1khz_rms"] = DataChannelBuffer('U{:s}_HF_1kHz_rms'.format(self.name), sample_dimension=num_bands, agg_type='rms', unit="V")
|
|
839
|
+
|
|
790
840
|
# Create Current Channels
|
|
791
841
|
if self._i_channel:
|
|
792
842
|
self._calc_channels["one_period"]["current"] = {}
|
pqopen/storagecontroller.py
CHANGED
|
@@ -57,11 +57,13 @@ import numpy as np
|
|
|
57
57
|
from pqopen.helper import floor_timestamp, JsonDecimalLimiter
|
|
58
58
|
from pqopen.eventdetector import Event
|
|
59
59
|
from daqopen.channelbuffer import DataChannelBuffer, AcqBuffer
|
|
60
|
+
from daqopen.daqinfo import DaqInfo
|
|
60
61
|
from pathlib import Path
|
|
61
62
|
from typing import List, Dict
|
|
62
63
|
import logging
|
|
63
64
|
import json
|
|
64
65
|
import gzip
|
|
66
|
+
import time
|
|
65
67
|
import paho.mqtt.client as mqtt
|
|
66
68
|
|
|
67
69
|
|
|
@@ -91,7 +93,7 @@ class StorageEndpoint(object):
|
|
|
91
93
|
"""
|
|
92
94
|
Writes aggregated data to the storage endpoint.
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
Parameters:
|
|
95
97
|
data: The aggregated data to store.
|
|
96
98
|
timestamp_us: The timestamp in microseconds for the aggregated data.
|
|
97
99
|
interval_seconds: The aggregation interval in seconds.
|
|
@@ -101,6 +103,17 @@ class StorageEndpoint(object):
|
|
|
101
103
|
def write_event(self, event: Event):
|
|
102
104
|
pass
|
|
103
105
|
|
|
106
|
+
def write_measurement_config(self, m_config: dict, daq_info: DaqInfo, channe_info: dict, **kwargs):
|
|
107
|
+
"""
|
|
108
|
+
Writes measurement config to the storage endpoint.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
m_config: The measurement config to store.
|
|
112
|
+
daq_info: The daq info to store.
|
|
113
|
+
channel_info: The channel's specific infos
|
|
114
|
+
"""
|
|
115
|
+
pass
|
|
116
|
+
|
|
104
117
|
class StoragePlan(object):
|
|
105
118
|
"""Defines a plan for storing data with specified intervals and channels."""
|
|
106
119
|
|
|
@@ -190,6 +203,9 @@ class StoragePlan(object):
|
|
|
190
203
|
if self._store_events_enabled:
|
|
191
204
|
self.storage_endpoint.write_event(event, **self._additional_config)
|
|
192
205
|
|
|
206
|
+
def store_measurement_config(self, m_config: dict, daq_info: DaqInfo, channel_info: dict):
|
|
207
|
+
self.storage_endpoint.write_measurement_config(m_config, daq_info, channel_info, **self._additional_config)
|
|
208
|
+
|
|
193
209
|
class StorageController(object):
|
|
194
210
|
"""Manages multiple storage plans and processes data for storage."""
|
|
195
211
|
|
|
@@ -304,7 +320,10 @@ class StorageController(object):
|
|
|
304
320
|
available_channels: dict,
|
|
305
321
|
measurement_id: str,
|
|
306
322
|
device_id: str,
|
|
307
|
-
start_timestamp_us: int
|
|
323
|
+
start_timestamp_us: int,
|
|
324
|
+
m_config: dict | None = None,
|
|
325
|
+
daq_info: DaqInfo | None = None,
|
|
326
|
+
channel_info: dict| None = None):
|
|
308
327
|
"""
|
|
309
328
|
Setup endpoints and storage plans from config
|
|
310
329
|
|
|
@@ -319,7 +338,7 @@ class StorageController(object):
|
|
|
319
338
|
Raises:
|
|
320
339
|
NotImplementedError: If a not implemented endpoint will be configured
|
|
321
340
|
"""
|
|
322
|
-
self._configured_eps = {}
|
|
341
|
+
self._configured_eps: dict[str, StorageEndpoint] = {}
|
|
323
342
|
for ep_type, ep_config in endpoints.items():
|
|
324
343
|
if ep_type == "csv":
|
|
325
344
|
csv_storage_endpoint = CsvStorageEndpoint("csv", measurement_id, ep_config.get("data_dir", "/tmp/"))
|
|
@@ -375,8 +394,11 @@ class StorageController(object):
|
|
|
375
394
|
else:
|
|
376
395
|
logger.warning(f"Channel {channel} not available for storing")
|
|
377
396
|
self.add_storage_plan(storage_plan)
|
|
378
|
-
|
|
379
|
-
|
|
397
|
+
# Initially store the measurement config if configured
|
|
398
|
+
if sp_config.get("store_config", False) and m_config and daq_info and channel_info:
|
|
399
|
+
storage_plan.store_measurement_config(m_config=m_config,
|
|
400
|
+
daq_info=daq_info,
|
|
401
|
+
channel_info=channel_info)
|
|
380
402
|
|
|
381
403
|
class TestStorageEndpoint(StorageEndpoint):
|
|
382
404
|
"""A implementation of StorageEndpoint for testing purposes."""
|
|
@@ -499,6 +521,27 @@ class MqttStorageEndpoint(StorageEndpoint):
|
|
|
499
521
|
self._client.publish(topic_prefix + f"/{self._device_id:s}/event/json",
|
|
500
522
|
json_item.encode('utf-8'), qos=2)
|
|
501
523
|
|
|
524
|
+
def write_measurement_config(self, m_config: dict, daq_info: DaqInfo, channel_info: dict, **kwargs):
|
|
525
|
+
"""
|
|
526
|
+
Write measurement config
|
|
527
|
+
"""
|
|
528
|
+
m_config_object = {
|
|
529
|
+
"type": "m_config",
|
|
530
|
+
"measurement_uuid": self.measurement_id,
|
|
531
|
+
"timestamp": time.time(),
|
|
532
|
+
"m_config": m_config,
|
|
533
|
+
"daq_info": daq_info.to_dict(),
|
|
534
|
+
"channel_info": channel_info
|
|
535
|
+
}
|
|
536
|
+
json_item = json.dumps(m_config_object)
|
|
537
|
+
topic_prefix = kwargs.get("mqtt_topic_prefix", self._topic_prefix)
|
|
538
|
+
if self._compression:
|
|
539
|
+
self._client.publish(topic_prefix + f"/{self._device_id:s}/m_config/gjson",
|
|
540
|
+
gzip.compress(json_item.encode('utf-8')), qos=2)
|
|
541
|
+
else:
|
|
542
|
+
self._client.publish(topic_prefix + f"/{self._device_id:s}/m_config/json",
|
|
543
|
+
json_item.encode('utf-8'), qos=2)
|
|
544
|
+
|
|
502
545
|
class HomeAssistantStorageEndpoint(StorageEndpoint):
|
|
503
546
|
"""Represents a MQTT endpoint (MQTT) for HomeAssistant."""
|
|
504
547
|
def __init__(self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pqopen-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A power quality processing library for calculating parameters from waveform data
|
|
5
5
|
Project-URL: Homepage, https://github.com/DaqOpen/pqopen-lib
|
|
6
6
|
Project-URL: Issues, https://github.com/DaqOpen/pqopen-lib/issues
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pqopen/__init__.py,sha256=sMVEOm5j6AZYnnEox5PHOUyZlL5TJjpsNMm5ATLx6ec,329
|
|
2
|
+
pqopen/auxcalc.py,sha256=P11Nu9pgJRoPYZDjLk-mXI6Ha02LoTH5bS9FdFTvC9M,2733
|
|
3
|
+
pqopen/eventdetector.py,sha256=NKZU7GbeorZdkYu3ET4lhMaeynw70GhIGO2p1xH4aTA,11962
|
|
4
|
+
pqopen/helper.py,sha256=0msrm6i1v8jj2Z5X8F7wDEW0KD5i91RBNZwPJC05YrA,2533
|
|
5
|
+
pqopen/powerquality.py,sha256=Cg7rwXOo3S63idE4XmrlC8CXHgGHbqI6-Urmhlg6ySc,16558
|
|
6
|
+
pqopen/powersystem.py,sha256=1Fi5SbS4PsUNz3hANjJBzTNtuM5bvQ6Oivae80K6RXI,52415
|
|
7
|
+
pqopen/storagecontroller.py,sha256=8JkeRX3HDWGFR40wO1ejxctQp53eMgEtsJMbOl7ea1Y,33468
|
|
8
|
+
pqopen/zcd.py,sha256=olJZsHRd1CjU65vysc2emR5wFGMkayIWdUEDNE8jfIc,6253
|
|
9
|
+
pqopen_lib-0.9.0.dist-info/METADATA,sha256=-UoUljXNQMLQUuR0GE_bBZpBY_sdBl5k5G1ZZ8GCwOw,4787
|
|
10
|
+
pqopen_lib-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
pqopen_lib-0.9.0.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
|
|
12
|
+
pqopen_lib-0.9.0.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pqopen/__init__.py,sha256=sMVEOm5j6AZYnnEox5PHOUyZlL5TJjpsNMm5ATLx6ec,329
|
|
2
|
-
pqopen/auxcalc.py,sha256=P11Nu9pgJRoPYZDjLk-mXI6Ha02LoTH5bS9FdFTvC9M,2733
|
|
3
|
-
pqopen/eventdetector.py,sha256=NKZU7GbeorZdkYu3ET4lhMaeynw70GhIGO2p1xH4aTA,11962
|
|
4
|
-
pqopen/helper.py,sha256=0msrm6i1v8jj2Z5X8F7wDEW0KD5i91RBNZwPJC05YrA,2533
|
|
5
|
-
pqopen/powerquality.py,sha256=dRVCedWa1QJKHgdiYoIIdvhH_p40cwpgeUePO5u1j28,15953
|
|
6
|
-
pqopen/powersystem.py,sha256=BDB-Xf-xe3UYVBUyX2gjKXnWpeZWTQTaLDSGCvtmGZ4,49256
|
|
7
|
-
pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
|
|
8
|
-
pqopen/zcd.py,sha256=olJZsHRd1CjU65vysc2emR5wFGMkayIWdUEDNE8jfIc,6253
|
|
9
|
-
pqopen_lib-0.8.4.dist-info/METADATA,sha256=hC1fidnpdel5XKb8T5zDJ6H3zidLNtTTJuhnYnKAVQE,4787
|
|
10
|
-
pqopen_lib-0.8.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
-
pqopen_lib-0.8.4.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
|
|
12
|
-
pqopen_lib-0.8.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|