mercury-base 2.0.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.
- mercury_base/__init__.py +56 -0
- mercury_base/exceptions.py +54 -0
- mercury_base/mercury_v1/README.md +187 -0
- mercury_base/mercury_v1/__init__.py +52 -0
- mercury_base/mercury_v1/commands.py +864 -0
- mercury_base/mercury_v2/README.md +242 -0
- mercury_base/mercury_v2/__init__.py +66 -0
- mercury_base/mercury_v2/commands.py +1056 -0
- mercury_base/meter.py +383 -0
- mercury_base/py.typed +1 -0
- mercury_base/transport.py +242 -0
- mercury_base/utils.py +72 -0
- mercury_base-2.0.0.dist-info/METADATA +274 -0
- mercury_base-2.0.0.dist-info/RECORD +17 -0
- mercury_base-2.0.0.dist-info/WHEEL +5 -0
- mercury_base-2.0.0.dist-info/licenses/LICENSE.md +13 -0
- mercury_base-2.0.0.dist-info/top_level.txt +1 -0
mercury_base/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Набор инструментов для связи со счётчиками Инкотекс Меркурий по последовательной
|
|
3
|
+
шине или TCP.
|
|
4
|
+
|
|
5
|
+
Пакет — тонкий стек из двух слоёв. `DataTransport` передаёт байты устройству и
|
|
6
|
+
обратно и ничего не знает о протоколе; `Meter` знает протокол своего устройства,
|
|
7
|
+
проверяет кадры, которые вернул транспорт, и называет команды счётчика. `Meters`
|
|
8
|
+
хранит набор счётчиков — это то, что нужно шлюзу, опрашивающему несколько
|
|
9
|
+
устройств.
|
|
10
|
+
|
|
11
|
+
Поддерживаются два протокола, каждый в своём подпакете: `mercury_v1` для
|
|
12
|
+
однофазных счётчиков (200, 201, 203, 206) и `mercury_v2` для трёхфазных
|
|
13
|
+
(203.2TD, 204, 208, 230, 231, 234, 236, 238). Протокол определяется по ответу
|
|
14
|
+
самого счётчика, поэтому вызывающий передаёт только адрес.
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2023–2026 webtoucher
|
|
17
|
+
Распространяется по лицензии BSD 3-Clause. Подробности в LICENSE.md.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from mercury_base.exceptions import (
|
|
21
|
+
CheckSumError,
|
|
22
|
+
CommunicationError,
|
|
23
|
+
ConnectError,
|
|
24
|
+
MercuryError,
|
|
25
|
+
TransportError,
|
|
26
|
+
UnexpectedAddress,
|
|
27
|
+
UnexpectedCommand,
|
|
28
|
+
UnknownCommandError,
|
|
29
|
+
)
|
|
30
|
+
from mercury_base.meter import Meter, MeterInfo, Meters
|
|
31
|
+
from mercury_base.transport import (
|
|
32
|
+
DataTransport,
|
|
33
|
+
SerialDataTransport,
|
|
34
|
+
TcpDataTransport,
|
|
35
|
+
transport_from_port,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Meter",
|
|
40
|
+
"MeterInfo",
|
|
41
|
+
"Meters",
|
|
42
|
+
"DataTransport",
|
|
43
|
+
"SerialDataTransport",
|
|
44
|
+
"TcpDataTransport",
|
|
45
|
+
"transport_from_port",
|
|
46
|
+
"MercuryError",
|
|
47
|
+
"TransportError",
|
|
48
|
+
"ConnectError",
|
|
49
|
+
"CommunicationError",
|
|
50
|
+
"UnexpectedAddress",
|
|
51
|
+
"UnexpectedCommand",
|
|
52
|
+
"CheckSumError",
|
|
53
|
+
"UnknownCommandError",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
__version__ = "2.0.0"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ошибки, которые вызывает набор инструментов.
|
|
3
|
+
|
|
4
|
+
Они образуют два небольших дерева: `TransportError` означает, что кадр не дошёл
|
|
5
|
+
до счётчика или ответ не вернулся, а `CommunicationError` — что что-то вернулось,
|
|
6
|
+
но это не ответ на запрос. Перехват `MercuryError` покрывает оба случая, именно
|
|
7
|
+
так и работает автоопределение счётчика.
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2023–2026 webtoucher
|
|
10
|
+
Распространяется по лицензии BSD 3-Clause. Подробности в LICENSE.md.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MercuryError",
|
|
15
|
+
"TransportError",
|
|
16
|
+
"ConnectError",
|
|
17
|
+
"CommunicationError",
|
|
18
|
+
"UnexpectedAddress",
|
|
19
|
+
"UnexpectedCommand",
|
|
20
|
+
"CheckSumError",
|
|
21
|
+
"UnknownCommandError",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MercuryError(Exception):
|
|
26
|
+
"""Общий предок всех ошибок набора инструментов."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TransportError(MercuryError):
|
|
30
|
+
"""Транспорт не удаётся открыть, либо открытый транспорт отказал."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConnectError(MercuryError):
|
|
34
|
+
"""По адресу не ответил ни один известный счётчик, поэтому подключения нет."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CommunicationError(MercuryError):
|
|
38
|
+
"""Ответ не пришёл или он не соответствует запросу."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnexpectedAddress(CommunicationError):
|
|
42
|
+
"""Ответ адресован другому счётчику — признак перепутанной шины."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnexpectedCommand(CommunicationError):
|
|
46
|
+
"""Ответ несёт другую команду — признак запоздавшего ответа."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CheckSumError(CommunicationError):
|
|
50
|
+
"""Кадр не проходит CRC-16, то есть повреждён или обрезан в пути."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UnknownCommandError(MercuryError, AttributeError):
|
|
54
|
+
"""В протоколе счётчика нет команды с таким именем."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Однофазные счётчики Инкотекс Меркурий 200, 201, 203 и 206
|
|
2
|
+
|
|
3
|
+
Драйвер `mercury_base.mercury_v1`. Кадр запроса — четырёхбайтовый сетевой адрес в
|
|
4
|
+
сетевом порядке, байт команды и данные; кадр ответа — тот же адрес, тот же байт
|
|
5
|
+
команды и данные. Числа счётчик присылает старшим байтом вперёд, а время и дату —
|
|
6
|
+
двоично-десятичным кодом, поэтому одни ответы читаются как двоичные, а другие —
|
|
7
|
+
как пары десятичных цифр.
|
|
8
|
+
|
|
9
|
+
Разбор опирается на «Систему команд счётчиков М206» (Incotex, 01.04.2018) из
|
|
10
|
+
[раздела документации по протоколам](https://www.incotexcom.ru/support/docs/protocol).
|
|
11
|
+
Все команды проверены на живом счётчике Меркурий 206: кадры его ответов стали
|
|
12
|
+
фикстурами тестов, а разобранные значения сошлись с конфигуратором прибора.
|
|
13
|
+
|
|
14
|
+
## Команды
|
|
15
|
+
|
|
16
|
+
Команды идут от базовых к продвинутым — в том же порядке, что и в
|
|
17
|
+
`commands.py`: сначала опознание прибора и время, затем учёт энергии и
|
|
18
|
+
мгновенные значения, после них лимиты, архивы и максимумы, а в конце
|
|
19
|
+
расписания, индикация и журналы.
|
|
20
|
+
|
|
21
|
+
### Опознание прибора
|
|
22
|
+
|
|
23
|
+
| Команда | Код | Параметры | Результат |
|
|
24
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
25
|
+
| interview | — | | данные для автоопределения (`dict`) или `None`, если протокол не подошёл |
|
|
26
|
+
| get_serial_number | 2Fh | | серийный номер (`int`) |
|
|
27
|
+
| get_info | 86h | 255 | модель (`str` или `None`) и комплектация (`list[str]`) |
|
|
28
|
+
| get_group_address | 20h | | групповой адрес строкой из восьми шестнадцатеричных цифр (`str`) |
|
|
29
|
+
| get_manufacture_date | 66h | | дата изготовления прибора, `Y-m-d` (`str`) |
|
|
30
|
+
| get_firmware_info | 28h | | версия прошивки и её дата (`dict`) |
|
|
31
|
+
| get_execution_word | 65h | | реле и второй датчик тока (`dict`) |
|
|
32
|
+
| get_tariffs_count | 2Eh | | число действующих тарифов (`int`) |
|
|
33
|
+
| get_timeout_multiplier | 82h | | множитель таймаута обмена (`int`) |
|
|
34
|
+
|
|
35
|
+
### Время прибора
|
|
36
|
+
|
|
37
|
+
| Команда | Код | Параметры | Результат |
|
|
38
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
39
|
+
| get_datetime | 21h | | текущие дата и время, `Y-m-d H:M:S` (`str`) |
|
|
40
|
+
| get_is_seasonal_time | 24h | | разрешён ли переход на летнее и зимнее время (`bool`) |
|
|
41
|
+
| get_time_correction | 25h | | предел коррекции хода часов в минутах (`int`) |
|
|
42
|
+
|
|
43
|
+
### Учёт энергии
|
|
44
|
+
|
|
45
|
+
| Команда | Код | Параметры | Результат |
|
|
46
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
47
|
+
| get_energy_accumulators | 27h | | тарифные аккумуляторы активной энергии 1–4 в киловатт-часах (`list[float]`) |
|
|
48
|
+
| get_reactive_energy_accumulators | 85h | | тарифные аккумуляторы реактивной энергии (`list[float]`) |
|
|
49
|
+
| get_tariff | 60h | | действующий тариф (`int`) |
|
|
50
|
+
| get_month_energy_slices | 32h | 0…B | показания на начало месяца по тарифам 1–4 (`list[float]`) |
|
|
51
|
+
| get_reactive_month_energy_slices | 38h | 0…B | показания на начало месяца в реактивной энергии (`list[float]`) |
|
|
52
|
+
|
|
53
|
+
### Мгновенные значения
|
|
54
|
+
|
|
55
|
+
| Команда | Код | Параметры | Результат |
|
|
56
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
57
|
+
| get_load_power | 26h | | текущая мощность в нагрузке в киловаттах (`float`) |
|
|
58
|
+
| get_vcp | 63h | | напряжение, ток и мощность в нагрузке (`dict`) |
|
|
59
|
+
| get_reactive_power | 86h | 0 | реактивная мощность в киловарах (`float`) |
|
|
60
|
+
| get_full_power_and_cos_fi | 86h | 2 | коэффициент мощности и полная мощность (`dict`) |
|
|
61
|
+
| get_mains_parameters | 81h | | частота сети, действующий тариф и флаги качества (`dict`) |
|
|
62
|
+
| get_battery_voltage | 29h | | напряжение на литиевой батарее в вольтах (`float`) |
|
|
63
|
+
|
|
64
|
+
### Лимиты и управление нагрузкой
|
|
65
|
+
|
|
66
|
+
| Команда | Код | Параметры | Результат |
|
|
67
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
68
|
+
| get_power_limit | 22h | | лимит мощности в киловаттах (`float`) |
|
|
69
|
+
| get_month_energy_limit | 23h | | лимит энергии за месяц в киловатт-часах (`float`) |
|
|
70
|
+
| get_tariff_energy_limits | 6Eh | | потарифные лимиты энергии и отметка превышения (`list[dict]`) |
|
|
71
|
+
| get_is_relay_on | 86h | 1 | включено ли реле (`bool`) |
|
|
72
|
+
| get_relay_mode | 6Dh | | чем управляется реле: `limits`, `off` или `on` (`str`) |
|
|
73
|
+
| get_output_optocoupler_function | 2Dh | | назначение выходного оптрона (`int`) |
|
|
74
|
+
|
|
75
|
+
### Архивы и максимумы
|
|
76
|
+
|
|
77
|
+
| Команда | Код | Параметры | Результат |
|
|
78
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
79
|
+
| get_month_energy_slice | 37h | 1, m, y | те же показания, но с годом — срезы за четыре года (`dict`) |
|
|
80
|
+
| get_daily_energy_slice | 37h | 0, d, m, y | показания на начало суток и их дата (`dict`) |
|
|
81
|
+
| get_half_hour_energies | 37h | 10h…15h, d, m, y | восемь получасовок на дату по 0,2 Вт·ч (`list[float]`) |
|
|
82
|
+
| get_archive_type | 37h | E0h | глубина архивов прибора (`int`) |
|
|
83
|
+
| get_maxima | 33h | 0, 1 и 2 | максимум величины и момент, когда он достигнут (`dict`) |
|
|
84
|
+
|
|
85
|
+
### Расписания и индикация
|
|
86
|
+
|
|
87
|
+
| Команда | Код | Параметры | Результат |
|
|
88
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
89
|
+
| get_tariff_schedule | 31h | 0…B7h | точки переключения тарифов: время и номер тарифа (`list[dict]`) |
|
|
90
|
+
| get_holidays | 30h | 0 и 1 | праздничные дни в виде `m-d` (`list[str]`) |
|
|
91
|
+
| get_display_filters | 2Ah | | маска индикации строкой из восьми битов (`str`) |
|
|
92
|
+
| get_display_times | 67h | | время индикации четырёх экранов в секундах (`list[int]`) |
|
|
93
|
+
| get_display_mode | 6Ah | | маска дополнительной индикации и её задержка (`dict`) |
|
|
94
|
+
| get_is_under_battery_display | 6Fh | | разрешена ли индикация под батарейкой (`bool`) |
|
|
95
|
+
|
|
96
|
+
### Журналы и диагностика
|
|
97
|
+
|
|
98
|
+
| Команда | Код | Параметры | Результат |
|
|
99
|
+
|------------------------------------|-----|------------------|-----------------------------------------------------------------------------|
|
|
100
|
+
| get_last_stop_datetime | 2Bh | | время последнего отключения напряжения, `Y-m-d H:M:S` (`str`) |
|
|
101
|
+
| get_last_start_datetime | 2Ch | | время последнего включения напряжения, `Y-m-d H:M:S` (`str`) |
|
|
102
|
+
| get_last_cover_open_datetime | 61h | | время последнего вскрытия крышки, `Y-m-d H:M:S` (`str`) |
|
|
103
|
+
| get_last_cover_close_datetime | 62h | | время последнего закрытия крышки, `Y-m-d H:M:S` (`str`) |
|
|
104
|
+
| get_last_parameterization_datetime | 6Bh | | время последней параметризации, `Y-m-d H:M:S` (`str`) |
|
|
105
|
+
| get_operating_time | 69h | | наработка под напряжением и без него в часах (`dict`) |
|
|
106
|
+
| get_modem_info | 6Ch | | номер модема и уровень сигнала (`dict`) |
|
|
107
|
+
| get_is_plc_modem_enabled | 80h | | разрешён ли модем PLC2 (`bool`) |
|
|
108
|
+
| get_switch_events | 34h | 0…3Fh | событие включения или выключения и его время (`dict`) |
|
|
109
|
+
| get_cover_events | 35h | 0…3Fh | событие вскрытия или закрытия крышки и его время (`dict`) |
|
|
110
|
+
| get_parameterization_events | 36h | 0…3Fh | дата параметризации и коды записанных команд (`dict`) |
|
|
111
|
+
| get_quality_events | 39h | ii6, ii7 | событие качества электроэнергии и его время (`dict`) |
|
|
112
|
+
|
|
113
|
+
## Примечания
|
|
114
|
+
|
|
115
|
+
- Команды `get_info`, `get_is_relay_on`, `get_full_power_and_cos_fi` и
|
|
116
|
+
`get_reactive_power` работают с байтом 86h; в описании протокола он есть, но
|
|
117
|
+
восстанавливать его пришлось по записям обмена с реальным счётчиком.
|
|
118
|
+
Незнакомый код модели оставляет `model` незаполненным, а список признаков —
|
|
119
|
+
пустым.
|
|
120
|
+
- Мгновенные значения приходят целыми: лимит мощности и мощность в нагрузке — в
|
|
121
|
+
сотых долях киловатта (`mm,mm`), поэтому функция делит их на 100. Напряжение,
|
|
122
|
+
ток и мощность команды 63h передаются двоично-десятичным кодом и делятся на 10,
|
|
123
|
+
100 и 1000, то есть возвращаются в вольтах, амперах и киловаттах.
|
|
124
|
+
- Лимит энергии за месяц — целые киловатт-часы без дробной части: наибольшее
|
|
125
|
+
значение поля, 9999 кВт⋅ч, счётчик держит, когда лимит не задан.
|
|
126
|
+
- Аккумуляторы энергии приходят в десятках ватт-часов, поэтому функция делит их на
|
|
127
|
+
100 и возвращает киловатт-часы.
|
|
128
|
+
- Время и дата — семь байтов двоично-десятичного кода `dow-hh-mm-ss-dd-mon-yy`:
|
|
129
|
+
день недели в них первый, поэтому команды отбрасывают один байт, а не два.
|
|
130
|
+
Год передаётся двумя последними цифрами.
|
|
131
|
+
- Таблица праздников состоит из шестнадцати дат, разбитых на две половины; каждая
|
|
132
|
+
дата — число и месяц в двоично-десятичном коде, а пустая дата заполнена
|
|
133
|
+
единицами. Функция читает обе половины и разбирает дату без `strptime`: день
|
|
134
|
+
без года в нём допустим не во всех версиях Python.
|
|
135
|
+
- Маска индикации: биты 0–3 разрешают индикацию тарифов 1–4, bit4 — суммы,
|
|
136
|
+
bit5 — мощности, bit6 — времени, bit7 — даты; нулевая маска оставляет только
|
|
137
|
+
индикацию текущего тарифа.
|
|
138
|
+
- Групповой адрес позволяет одной командой настроить всю группу: такие счётчики
|
|
139
|
+
команду выполняют, но не отвечают.
|
|
140
|
+
- Параметры команды занимают по одному байту, поэтому `get_holidays` и
|
|
141
|
+
`get_is_relay_on` передают номер половины таблицы и номер функции отдельными
|
|
142
|
+
вызовами.
|
|
143
|
+
- `get_tariff` на однотарифном счётчике отвечает нулём, хотя описание протокола
|
|
144
|
+
обещает 1–4. Расписание (команда 31h) при этом хранится и делит сутки на зоны
|
|
145
|
+
`T1` и `T2`, но счётчик его не применяет и отвечает нулём и до границы 23:00,
|
|
146
|
+
и после неё.
|
|
147
|
+
- Дата изготовления (66h) — три байта `dd-mon-yy`, как в паспорте прибора. Ключ
|
|
148
|
+
`date_of_manufacture` в ответе `get_firmware_info` — это не она, а дата версии
|
|
149
|
+
прошивки из команды 28h: на проверенном приборе они расходятся.
|
|
150
|
+
- Наработка (69h) — две трёхбайтовые величины в часах: время под напряжением и
|
|
151
|
+
время без напряжения. Первое поле растёт, пока на счётчике есть питание.
|
|
152
|
+
- Коэффициент мощности приходит двумя байтами двоично-десятичного кода, а знак —
|
|
153
|
+
старший бит старшего байта. На всех записанных кадрах знак был положительным,
|
|
154
|
+
а конфигуратор прибора показывает тот же модуль для индуктивной нагрузки со
|
|
155
|
+
знаком минус: минус — соглашение конфигуратора, а не поле кадра.
|
|
156
|
+
- Расписание (31h) — шестнадцать пар «час-минута», и в двух старших битах часа
|
|
157
|
+
записан номер тарифа с нуля. Параметр — месяц в старшей тетраде и день недели
|
|
158
|
+
в младшей (0 — воскресенье, 7 — праздник). На проверенном приборе все дни и
|
|
159
|
+
месяцы отдают одну и ту же таблицу.
|
|
160
|
+
- Максимумы (33h) — по две записи в ответе: несбрасываемый максимум и такой же,
|
|
161
|
+
который сбрасывает команда 12h. Каждая запись — два байта значения и шесть
|
|
162
|
+
байт времени. Мощность приходит полем «`mm,mm` кВт», как у команды 26h, поэтому
|
|
163
|
+
делится на 100, а не на 1000, как трёхбайтовая мощность команды 63h.
|
|
164
|
+
- Срезы (32h, 37h, 38h) и аккумуляторы реактивной энергии (85h) — это не расход
|
|
165
|
+
за сутки или месяц, а показания счётчика на их начало. Расход — разность двух
|
|
166
|
+
соседних срезов: на проверенном приборе срез 01.11 дал 12984,11 кВт⋅ч, срез
|
|
167
|
+
15.11 — 13136,57, а текущие аккумуляторы — 13147,62 кВт⋅ч.
|
|
168
|
+
- В команде 37h параметр `N48` выбирает архив: 0 — суточные срезы, 1 — месячные
|
|
169
|
+
срезы за четыре года (года у команды 32h нет), 10h–15h — группы получасовок с
|
|
170
|
+
10h, E0h — глубина архивов, F0h — «потерянные» получасовки. Ответ получасовок —
|
|
171
|
+
восемь записей по четыре байта: два байта энергии младшим байтом вперёд с
|
|
172
|
+
разрешением 0,2 Вт⋅ч и два байта контрольной суммы записи.
|
|
173
|
+
- Журналы (34h–36h, 39h) отдают записи от свежих к старым, поэтому параметр 0 —
|
|
174
|
+
последнее событие. В журнале параметризации (36h) три слова битовых флагов
|
|
175
|
+
отвечают за команды 00h–0Fh, 10h–17h и 70h–7Fh: на проверенном приборе
|
|
176
|
+
выставлен бит команды 0Ah, которой записывают тариф.
|
|
177
|
+
- В журнале качества (39h) событие лежит в двух старших битах часа: 80h — переход
|
|
178
|
+
порога, 40h — возврат к норме. Смысл параметров `ii6` и `ii7` описание не
|
|
179
|
+
раскрывает, поэтому функция называет их `index` и `kind` и передаёт как есть.
|
|
180
|
+
- Потарифные лимиты (6Eh) — четыре записи по четыре байта: три байта лимита в
|
|
181
|
+
двоично-десятичном коде и байт флага, где AAh отмечает превышение. На приборе
|
|
182
|
+
лимит первого тарифа — 986852 кВт⋅ч, а конфигуратор показывает на 100000
|
|
183
|
+
меньше: разницу он вычитает как уже израсходованную энергию.
|
|
184
|
+
- Не реализованы четыре кода из описания: 3Ah (журнал реле) и 86h с параметром 3
|
|
185
|
+
(автовключение реле) проверенный прибор оставляет без ответа; у 64h
|
|
186
|
+
(коэффициент коррекции хода) и 68h (режим лимита мощности) ответ приходит, но
|
|
187
|
+
знак и единицы первого и биты второго с конфигуратором не сходятся.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Протокол однофазных счётчиков: Меркурий 200, 201, 203 и 206.
|
|
3
|
+
|
|
4
|
+
Кадр начинается с четырёхбайтового адреса в сетевом порядке, затем идёт один
|
|
5
|
+
байт команды, затем данные и CRC-16 в конце. Адрес счётчика — это его серийный
|
|
6
|
+
номер, поэтому сетевой адрес может содержать до восьми цифр.
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2023–2026 webtoucher
|
|
9
|
+
Распространяется по лицензии BSD 3-Clause. Подробности в LICENSE.md.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from struct import pack, unpack
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from mercury_base.mercury_v1 import commands
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"commands",
|
|
19
|
+
"prepare_address",
|
|
20
|
+
"format_address",
|
|
21
|
+
"extract_address",
|
|
22
|
+
"extract_command",
|
|
23
|
+
"extract_data",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
#: Четыре байта в сетевом порядке.
|
|
27
|
+
ADDRESS_FORMAT = "!I"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def prepare_address(address: int) -> int:
|
|
31
|
+
"""Вернуть адрес, который может нести протокол, — последние восемь цифр."""
|
|
32
|
+
return address % 100000000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def format_address(address: int) -> bytes:
|
|
36
|
+
"""Вернуть адрес в виде четырёх байтов, с которых начинается кадр."""
|
|
37
|
+
return pack(ADDRESS_FORMAT, address)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def extract_address(message: bytes) -> int:
|
|
41
|
+
"""Вернуть адрес, которому адресован кадр или с которого он пришёл."""
|
|
42
|
+
return unpack(ADDRESS_FORMAT, message[:4])[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_command(message: bytes) -> int:
|
|
46
|
+
"""Вернуть байт команды, который несёт кадр."""
|
|
47
|
+
return message[4]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_data(message: bytes) -> List[int]:
|
|
51
|
+
"""Вернуть данные кадра без адреса, команды и CRC."""
|
|
52
|
+
return list(message[5:-2])
|