Compare commits

...

27 commits
v1 ... master

Author SHA1 Message Date
Michael Hope 23c0a49a26 vedirect: use the systemd watchdog
Some checks failed
continuous-integration/drone/push Build is failing
2021-05-09 14:20:10 +02:00
Michael Hope 66cb355392 vedirect: add the Phenix NVM group
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-18 14:34:22 +02:00
Michael Hope 0d26752550 vedirect: add a work-around for an empty serial number 2021-04-18 14:33:58 +02:00
Michael Hope 9a36e59d38 vedirect: fixed up the MPPT scale factors
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-28 17:45:23 +02:00
Michael Hope e14e5afa34 vedirect: looks stable, so poll less often.
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-21 17:02:01 +01:00
Michael Hope c682c172d8 vedirect: added frame metrics
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-21 16:30:39 +01:00
Michael Hope dda687d25b vedirect: add support for setting enums
Some checks failed
continuous-integration/drone/push Build is failing
2021-03-21 14:14:54 +01:00
Michael Hope 659546a35b vedirect: move the MPPT code out of cli.py and add detection.
Some checks failed
continuous-integration/drone/push Build is failing
2021-03-21 13:41:24 +01:00
Michael Hope 235f6b26df vedirect: move to the same register definition across products 2021-03-21 13:40:58 +01:00
Michael Hope 8c7bb58c6b vedirect: add registers for the Phoenix inverters. 2021-03-21 13:40:34 +01:00
Michael Hope 950548a9c3 vedirect: add flake8
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 21:28:14 +01:00
Michael Hope 32fde540f8 vedirect: fix the missing payload
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-07 20:14:55 +01:00
Michael Hope f0e797f2d6 vedirect: moved unpacking to hex.py
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-07 20:12:34 +01:00
Michael Hope 59ddf1a2c1 vedirect: move to drone for CI
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-07 20:04:38 +01:00
Michael Hope b521c25dd9 vedirect: put locks around the serial port usage 2021-02-28 11:30:18 +01:00
Michael Hope be80f12825 vedirect: on set, awaken the poller to get the new value 2021-02-28 11:19:14 +01:00
Michael Hope 83db956dfd vedirect: poll once a minute 2021-02-26 19:24:11 +01:00
Michael Hope 2469ebaed0 vedirect: use the serial number as the ID 2021-02-26 19:09:28 +01:00
Michael Hope 8bc6764988 vedirect: add the baud rate, dont die on a protocol error 2021-02-26 17:54:38 +00:00
Michael Hope 414a761918 vedirect: fix the apt-get deps command 2021-02-26 17:54:13 +01:00
Michael Hope f960699d2d vedirect: drop the power support, run flake8 2021-02-26 17:53:33 +01:00
Michael Hope 0f299386d9 vedirect: move to requirements.txt to pick up janet 2021-02-26 17:46:01 +01:00
Michael Hope 945a1c5a5f vedirect: tidy up the new HEX protocol. 2021-02-25 16:55:26 +01:00
Michael Hope c179cbc7c8 vedirect: move to the HEX protocol and janet libraries 2021-02-25 11:45:08 +01:00
Michael Hope 29f60a58f1 vedirect: ran YAPF and tidied up power.py 2021-01-02 17:37:49 +01:00
Michael Hope d74b2ec66b Merge branch 'master' of ssh://juju.nz:3022/michaelh/vedirect 2021-01-02 13:36:56 +00:00
Michael Hope b8057275a8 vedirect: add support for exporting directory entries 2021-01-02 13:33:31 +00:00
10 changed files with 1976 additions and 27 deletions

27
.drone.yml Normal file
View file

@ -0,0 +1,27 @@
kind: pipeline
name: default
platform:
os: linux
arch: arm64
steps:
- name: fetch
image: alpine/git
failure: ignore
commands:
- git fetch --tags
- name: build
image: python
commands:
- pip3 install -r requirements.txt
- python3 setup.py bdist
- name: test
image: python
commands:
- pip3 install -r requirements.txt
- pip3 install pytest flake8
- pytest vedirect
- flake8 vedirect

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.eggs
.mypy*
.vscode
build
dis
*.pyc
*.egg-info
*.tar.*

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
Pint>=0.16.1
click>=7.1.2
paho-mqtt>=1.5.1
prometheus-client>=0.8.0
pyserial>=3.4
git+https://juju.nz/src/michaelh/janet.git#egg=janet
systemd-python>=234

View file

@ -24,13 +24,6 @@ setup(
description='VE.Direct parser and exporter',
zip_safe=True,
packages=find_packages(),
install_requires=[
'Pint>=0.16.1',
'click>=7.1.2',
'paho-mqtt>=1.5.1',
'prometheus-client>=0.8.0',
'pyserial>=3.4',
],
entry_points='''
[console_scripts]
vedirect=vedirect.cli:app

View file

@ -12,47 +12,255 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import dataclasses
import enum
import logging
import queue
import struct
import pprint
import threading
import time
from dataclasses import dataclass
from importlib import metadata
from typing import Any, List, Union
import click
import janet
import janet.mqtt
import janet.prometheus
import prometheus_client
import serial
import systemd.daemon
from . import mqtt
from . import prometheus
from . import text
from . import hex, phoenix
from .hex import Register
from . import mppt
logger = logging.getLogger('vedirect.cli')
class Echo:
def export(self, fields):
print(fields)
class Echo(janet.Publisher):
def publish(self, device: janet.Device, entity: Any, unused_setter):
pprint.pprint(dataclasses.asdict(entity))
@dataclass
class Frame:
received: int
@dataclass
class Get:
register: int
data: bytes
@dataclass
class Done:
data: bytes
def _issubclass(typ, cls) -> bool:
try:
return issubclass(typ, cls)
except TypeError:
return False
class Poller:
def __init__(self, port: serial.Serial):
self._port = port
self._rx: 'queue.Queue[Union[Get,Done]]' = queue.Queue(100)
self._notify = threading.Condition()
self._lock = threading.Lock()
self._received = 0
def _discover(self):
while True:
with self._lock:
hex.get_product_id(self._port)
try:
resp = self._rx.get(timeout=1)
except queue.Empty:
continue
if not isinstance(resp, Done):
continue
pid = struct.unpack('<H', resp.data)[0]
for product in phoenix.PRODUCT_IDS:
if pid == product.id:
logger.info(f'Discovered a {product.name}')
return phoenix.GROUPS
if pid in mppt.PRODUCT_IDS:
logger.info('Discovered a %s' % mppt.PRODUCT_IDS[pid])
return mppt.GROUPS
raise RuntimeError(f'Unrecognised product ID {pid:X}')
def poll(self):
threading.Thread(target=self._getter).start()
groups = self._discover()
while True:
for group in groups:
values = {}
for field in dataclasses.fields(group):
register = field.metadata['vedirect.Register']
with self._lock:
hex.get(self._port, register.id)
while True:
try:
resp = self._rx.get(timeout=1)
if not isinstance(resp, Get):
continue
if resp.register == register.id:
values[field.name] = hex.unpack(
register, resp.data)
break
except queue.Empty:
logger.warning(
f'No response on {field.name}, skipping')
values[field.name] = None
break
yield group(**values)
yield Frame(self._received)
with self._notify:
self._notify.wait(60)
def set(self, field: dataclasses.Field, value: bytes) -> None:
value = value.decode('LATIN-1')
if _issubclass(field.type, enum.IntEnum):
try:
v = field.type[value.upper()]
except (KeyError, ValueError) as ex:
names = ' '.join(x.name.lower() for x in field.type)
logger.error(
f'Invalid enum value {value}, should be one of {names}',
exc_info=ex)
return
elif field.type in (float, int):
try:
v = float(value)
except ValueError as ex:
logger.warning(
(f'Value {value!r} for {field.name} is not a number, '
'dropping'),
exc_info=ex)
return
else:
logger.warning(f'Unsupported field type {field.type} '
f'for {field.name}, dropping')
return
if 'vedirect.Register' not in field.metadata:
logger.error(
f'Field {field.name} is not a MPPT register, dropping')
return
register: Register = field.metadata['vedirect.Register']
if register.scale is not None:
v /= register.scale
v = int(round(v))
logger.warning(f'set {field.name}={v}')
payload = struct.pack('<' + register.kind, v)
with self._lock:
hex.set(self._port, register.id, payload)
# TODO(michaelh): hack to give the set time to propagate.
time.sleep(0.5)
with self._notify:
self._notify.notify()
def _getter(self):
for frame in hex.decoder(self._port):
self._received += 1
if frame.command == hex.Response.GET:
self._get(frame)
elif frame.command == hex.Response.DONE:
self._done(frame)
elif frame.command == hex.Response.ASYNC:
pass
else:
logger.warning(f'Dropped {frame}')
def _get(self, frame):
register, flags = struct.unpack_from('<HB', frame.data, 0)
flags = hex.Flags(flags)
value = frame.data[3:]
if flags != hex.Flags.OK:
logger.warning(f'GET on {register:X} gave error {flags!r}')
return
self._rx.put(Get(register, value))
def _done(self, frame):
self._rx.put(Done(frame.data))
@click.command()
@click.option('--port',
type=click.Path(exists=True),
type=str,
required=True,
help='Serial port connected to the controller')
@click.option('--prometheus_port',
@click.option('--prometheus-port',
type=int,
help='If supplied, export metrics on this port')
@click.option('--mqtt_host',
@click.option('--mqtt-host',
help='If supplied, export metrics to this MQTT host')
@click.option('--echo',
is_flag=True,
help='If supplied, echo metrics to stdout')
def app(port: str, prometheus_port: int, mqtt_host: str, echo: bool):
s = serial.Serial(port, 19200, timeout=0.7)
exporters = []
logging.basicConfig(format='%(name)-12s %(levelname)-8s %(message)s',
level=logging.INFO)
s = serial.serial_for_url(port, baudrate=19200, timeout=0.7)
publishers: List[janet.Publisher] = []
if prometheus_port:
prometheus_client.start_http_server(prometheus_port)
exporters.append(prometheus.Exporter())
publishers.append(janet.prometheus.Client())
info = prometheus_client.Gauge('vedirect_info', 'Tool information',
('version', ))
info.labels(metadata.version('vedirect')).set(1)
if mqtt_host:
exporters.append(mqtt.Exporter(mqtt_host))
publishers.append(janet.mqtt.Client(mqtt_host))
if echo:
exporters.append(Echo())
publishers.append(Echo())
for fields in text.parse(s):
for e in exporters:
e.export(fields)
device = None
poller = Poller(s)
for g in poller.poll():
if isinstance(g, phoenix.Information) or isinstance(
g, mppt.Information):
if not g.serial_number:
continue
device = janet.Device(identifiers=[g.serial_number],
available=True,
model=g.model_name,
name=f'VE.Direct {g.serial_number}',
manufacturer='Vicron',
kind='vedirect')
systemd.daemon.notify('WATCHDOG=1')
if device is not None:
for publisher in publishers:
publisher.publish(device, g, poller.set)
if __name__ == '__main__':
app()

259
vedirect/hex.py Normal file
View file

@ -0,0 +1,259 @@
from __future__ import annotations
import abc
import dataclasses
import enum
import logging
import struct
from typing import Iterator, List, Sequence, Union, Type, Optional
import pint
logger = logging.getLogger('vedirect.hex')
_CR = ord('\n')
_COLON = ord(':')
_NIBBLES = {ord('0') + x: x for x in range(0, 10)}
_NIBBLES.update({ord('A') + x: 10 + x for x in range(0, 6)})
class Mode(enum.IntEnum):
UNSET = 0
R = 1
W = 2
RW = 3
@dataclasses.dataclass
class Register:
id: int
name: str
kind: str = ''
mode: Optional[Mode] = None
scale: Optional[float] = None
units: Union[str, Type[bool], Type[enum.Enum], None] = None
def Field(reg: Register) -> dataclasses.Field:
return dataclasses.field(default=dataclasses.MISSING,
metadata={'vedirect.Register': reg})
class Command(enum.IntEnum):
# 0x51FA51FA51FA51FA51FA as payload will enable bootloader mode.
ENTER_BOOT = 0
# Check for presence, the response is an Rsp ping containing version and
# firmware type.
PING = 1
# Returns the version of the firmware as stored in the header in an Rsp
# Done message.
VERSION = 3
# Returns the Product Id of the firmware as stored in the header in an
# Rsp,
PRODUCT_ID = 4
# Restarts the device, no response is sent.
RESTART = 6
# Returns a get response with the requested data or error is returned.
# Arguments are ID of the value to get (uint16) and flags (uint8, must be
# zero).
GET = 7
# Sets a value. Arguments are the value ID (uint16), flags (uint8, must be
# zero), and a type specific value.
SET = 8
# Asynchronous data message. Should not be replied. Arguments are flags
# (uint8) and a type specific value.
ASYNC = 0xA
class Response(enum.IntEnum):
DONE = 1
UNKNOWN = 3
ERROR = 4
PING = 5
GET = 7
SET = 8
ASYNC = 0xA
class Flags(enum.IntFlag):
OK = 0
UNKNOWN_ID = 1
NOT_SUPPORTED = 2
PARAMETER_ERROR = 4
class Port(abc.ABC):
@abc.abstractmethod
def write(self, data: bytes) -> None:
pass
@property
@abc.abstractmethod
def in_waiting(self) -> int:
return 0
@abc.abstractmethod
def read(self, want: int) -> bytes:
return b''
class ProtocolError(RuntimeError):
pass
def _reader(src: Port) -> Iterator[int]:
"""Generates bytes from a serial port."""
while True:
ready = max(1, src.in_waiting)
yield from src.read(ready)
def _get_line(src: Port) -> Iterator[Sequence[int]]:
"""Read a line from a port with None on idle."""
r = _reader(src)
while True:
while True:
ch = next(r)
if ch == _COLON:
break
line: List[int] = []
while True:
ch = next(r)
if ch == _CR:
yield line
break
elif ch == _COLON:
pass
else:
nibble = _NIBBLES.get(ch, None)
if nibble is None:
logger.warning(
f'Invalid character {ch!r} in {line!r}, dropping')
break
line.append(nibble)
@dataclasses.dataclass(frozen=True)
class Frame:
command: int
data: bytes
def decoder(src: Port) -> Iterator[Frame]:
"""Reads frames from the port yielding None on idle."""
for line in _get_line(src):
if len(line) <= 3:
logger.warning(f'Line is too short {line!r}')
continue
if len(line) % 2 != 1:
logger.warning(
f'Line should be an odd number of characters: {line!r}')
continue
command = Response(line[0])
data = bytes(
(line[x] << 4) + line[x + 1] for x in range(1, len(line), 2))
chk = (command + sum(data)) & 0xFF
if chk != 0x55:
logger.error(f'Checksum error sum={chk:x}')
continue
data = data[:-1]
yield Frame(command, data)
def get_uint32(data, offset: int) -> int:
return get_uint16(data, offset) | (get_uint16(data, offset + 2) << 16)
def get_uint16(data, offset: int) -> int:
return (data[offset + 1] << 8) | data[offset + 0]
def get_uint8(data, offset: int) -> int:
return data[offset]
class Packer:
def __init__(self):
self.payload = []
def add(self, ch: Union[int, bytes]) -> Packer:
if isinstance(ch, bytes):
self.payload.extend(ch)
else:
self.payload.append(ch)
return self
def add_uint16(self, v: int) -> Packer:
return self.add(v & 0xFF).add(v >> 8)
def add_uint32(self, v: int) -> Packer:
return self.add_uint16(v & 0xFFFF).add_uint16(v >> 16)
def sum(self) -> int:
return sum(self.payload)
def _issubclass(c, klass):
try:
return issubclass(c, klass)
except TypeError:
return False
def unpack(register, value: bytes) -> Union[str, float, int]:
"""Unpacks a register value."""
if register.kind == 'S':
data = value.decode('LATIN-1')
# Might be null-terminated.
return data.split('\0')[0]
data = struct.unpack_from('<' + register.kind, value, 0)[0]
if register.scale is not None:
data *= register.scale
data = round(data, 6)
if register.units is not None:
if _issubclass(register.units, enum.Enum):
data = register.units(int(data))
elif register.units in (bool, ):
data = register.units(data)
elif isinstance(register.units, pint.Unit):
data = pint.Quantity(data, register.units)
return data
def _send(port: Port, packer: Packer):
packer.add((0x55 - packer.sum()) & 0xFF)
payload = packer.payload
out = f':{payload[0]:X}' + ''.join('%02X' % x for x in payload[1:])
port.write(out.encode('LATIN-1') + b'\n')
def set(port: Port, register: int, value: bytes):
packer = Packer().add(
Command.SET.value).add_uint16(register).add(0).add(value)
_send(port, packer)
def get(port: Port, register: int) -> None:
packer = Packer().add(Command.GET.value).add_uint16(register).add(0)
_send(port, packer)
def get_version(port: Port) -> None:
packer = Packer().add(Command.VERSION.value)
_send(port, packer)
def get_product_id(port: Port) -> None:
packer = Packer().add(Command.PRODUCT_ID.value)
_send(port, packer)

694
vedirect/mppt.py Normal file
View file

@ -0,0 +1,694 @@
"""MPPT HEX Protocol specification."""
from dataclasses import dataclass
import enum
from . import schema
from .hex import Register, Field
import pint
_ureg = pint.get_application_registry()
class ID(enum.IntEnum):
# Product information registers
# 0x0100,Product Id,-,un32,-
PRODUCT_ID = 0x0100
# 0x0104,Group Id,-,un8,-
GROUP_ID = 0x0104
# 0x010A,Serial number,-,string,-
SERIAL_NUMBER = 0x010A
# 0x010B,Model name,-,string,-
MODEL_NAME = 0x010B
# 0x0140,Capabilities,-,un32,-
CAPABILITIES = 0x0140
# Generic device control registers
# 0x0200,Device mode,-,un8,-
DEVICE_MODE = 0x0200
# 0x0201,Device state,-,un8,-
DEVICE_STATE = 0x0201
# 0x0202,Remote control used,-,un32,-
REMOTE_CONTROL_USED = 0x0202
# 0x0205,Device off reason,-,un8,-
DEVICE_OFF_REASON = 0x0205
# 0x0207,Device off reason,-,un32,-
DEVICE_OFF_REASON_EXTENDED = 0x0207
# Battery settings registers
# 0xEDFF,Batterysafe mode,-,un8,0=off, 1=on
BATTERYSAFE_MODE = 0xEDFF
# 0xEDFE,Adaptive mode,-,un8,0=off, 1=on
ADAPTIVE_MODE = 0xEDFE
# 0xEDFD,Automatic equalisation mode,-,un8,0=off, 1..250
AUTOMATIC_EQUALISATION_MODE = 0xEDFD
# 0xEDFC,Battery bulk time limit,0.01,un16,hours
BATTERY_BULK_TIME_LIMIT = 0xEDFC
# 0xEDFB,Battery absorption time limit,0.01,un16,hours
BATTERY_ABSORPTION_TIME_LIMIT = 0xEDFB
# 0xEDF7,Battery absorption voltage,0.01,un16,V
BATTERY_ABSORPTION_VOLTAGE = 0xEDF7
# 0xEDF6,Battery float voltage,0.01,un16,V
BATTERY_FLOAT_VOLTAGE = 0xEDF6
# 0xEDF4,Battery equalisation voltage,0.01,un16,V
BATTERY_EQUALISATION_VOLTAGE = 0xEDF4
# 0xEDF2,Battery temp. compensation,0.01,sn16,mV/K
BATTERY_TEMP_COMPENSATION = 0xEDF2
# 0XEDF1,BATTERY TYPE,1,UN8,0XFF = user
BATTERY_TYPE = 0xEDF1
# 0xEDF0,Battery maximum current,0.1,un16,A
BATTERY_MAXIMUM_CURRENT = 0xEDF0
# 0xEDEF,Battery voltage,1,un8,V
BATTERY_VOLTAGE = 0xEDEF
# 0xEDEA,Battery voltage setting,1,un8,V
BATTERY_VOLTAGE_SETTING = 0xEDEA
# 0xEDE8,BMS present,-,un8,0=no, 1=yes
BMS_PRESENT = 0xEDE8
# 0xEDE7,Tail current,0.1,un16,
TAIL_CURRENT = 0xEDE7
# 0xEDE6,Low temperature charge current,0.1,un16,A, 0xFFFF=use max
LOW_TEMPERATURE_CHARGE_CURRENT = 0xEDE6
# 0xEDE5,Auto equalise stop on voltage,-,un8,0=no, 1=yes
AUTO_EQUALISE_STOP_ON_VOLTAGE = 0xEDE5
# 0xEDE4,Equalisation current level,1,un8,% (of 0xEDF0)
EQUALISATION_CURRENT_LEVEL = 0xEDE4
# 0xEDE3,Equalisation duration,0.01,un16,hours
EQUALISATION_DURATION = 0xEDE3
# 0xED2E,Re-bulk voltage offset,0.01,un16,V
RE_BULK_VOLTAGE_OFFSET = 0xED2E
# 0xEDE0,Battery low temperature level,0.01,sn16,°C
BATTERY_LOW_TEMPERATURE_LEVEL = 0xEDE0
# 0xEDCA,Voltage compensation,0.01,un16,V
VOLTAGE_COMPENSATION = 0xEDCA
# Charger data registers
# 0xEDEC,Battery temperature,0.01,un16,K
BATTERY_TEMPERATURE = 0xEDEC
# 0xEDDF,Charger maximum current,0.01,un16,A
CHARGER_MAXIMUM_CURRENT = 0xEDDF
# 0xEDDD,System yield,0.01,un32,kWh
SYSTEM_YIELD = 0xEDDD
# 0xEDDC,User yield (*2),0.01,un32,kWh
USER_YIELD = 0xEDDC
# 0xEDDB,Charger internal temperature,0.01,sn16,°C
CHARGER_INTERNAL_TEMPERATURE = 0xEDDB
# 0xEDDA,Charger error code,-,un8,-
CHARGER_ERROR_CODE = 0xEDDA
# 0xEDD7,Charger current,0.1,un16,A
CHARGER_CURRENT = 0xEDD7
# 0xEDD5,Charger voltage,0.01,un16,V
CHARGER_VOLTAGE = 0xEDD5
# 0xEDD4,Additional charger state info,-,un8,-
ADDITIONAL_CHARGER_STATE_INFO = 0xEDD4
# 0xEDD3,Yield today,0.01,(*4),kWh
YIELD_TODAY = 0xEDD3
# 0xEDD2,Maximum power today,1,un16,W
MAXIMUM_POWER_TODAY = 0xEDD2
# 0xEDD1,Yield yesterday,0.01,(*4),kWh
YIELD_YESTERDAY = 0xEDD1
# 0xEDD0,Maximum power yesterday,1,un16,W
MAXIMUM_POWER_YESTERDAY = 0xEDD0
# 0xEDCE,Voltage settings range,-,un16,-
VOLTAGE_SETTINGS_RANGE = 0xEDCE
# 0xEDCD,History version,-,un8,-
HISTORY_VERSION = 0xEDCD
# 0xEDCC,Streetlight version,-,un8,-
STREETLIGHT_VERSION = 0xEDCC
# 0x2211,Adjustable voltage minimum,0.01,un16,V
ADJUSTABLE_VOLTAGE_MINIMUM = 0x2211
# 0x2212 Adjustable voltage maximum 0.01 un16 V
ADJUSTABLE_VOLTAGE_MAXIMUM = 0x2212
# Solar panel data registers
# 0xEDBC,Panel power,0.01,un32,W
PANEL_POWER = 0xEDBC
# 0xEDBB,Panel voltage,0.01,un16,V
PANEL_VOLTAGE = 0xEDBB
# 0xEDBD,Panel current,0.1,un16,A
PANEL_CURRENT = 0xEDBD
# 0xEDB8,Panel maximum voltage,0.01,un16,V
PANEL_MAXIMUM_VOLTAGE = 0xEDB8
# 0xEDB3,Tracker mode,-,un8,-
TRACKER_MODE = 0xEDB3
# Load output data/settings registers
# 0xEDAD,Load current,0.1,un16,A
LOAD_CURRENT = 0xEDAD
# 0xEDAC,Load offset voltage,0.01,un16,V
LOAD_OFFSET_VOLTAGE = 0xEDAC
# 0xEDAB,Load output control,-,un8,-
LOAD_OUTPUT_CONTROL = 0xEDAB
# 0xEDA9,Load output voltage,0.01,un16,V
LOAD_OUTPUT_VOLTAGE = 0xEDA9
# 0xEDA8,Load output state,-,un8,-
LOAD_OUTPUT_STATE = 0xEDA8
# 0xED9D,Load switch high level,0.01,un16,V
LOAD_SWITCH_HIGH_LEVEL = 0xED9D
# 0xED9C,Load switch low level,0.01,un16,V
LOAD_SWITCH_LOW_LEVEL = 0xED9C
# 0xED91,Load output off reason,-,un8,-
LOAD_OUTPUT_OFF_REASON = 0xED91
# 0xED90,Load AES timer,1,un16,minute
LOAD_AES_TIMER = 0xED90
# Relay settings registers
# 0xEDD9,Relay operation mode,-,un8,-
RELAY_OPERATION_MODE = 0xEDD9
# 0x0350,Relay battery low voltage set,0.01,un16,V
RELAY_BATTERY_LOW_VOLTAGE_SET = 0x0350
# 0x0351,Relay battery low voltage clear,0.01,un16,V
RELAY_BATTERY_LOW_VOLTAGE_CLEAR = 0x0351
# 0x0352,Relay battery high voltage set,0.01,un16,V
RELAY_BATTERY_HIGH_VOLTAGE_SET = 0x0352
# 0x0353,Relay battery high voltage clear,0.01,un16,V
RELAY_BATTERY_HIGH_VOLTAGE_CLEAR = 0x0353
# 0xEDBA,Relay panel high voltage set,0.01,un16,V
RELAY_PANEL_HIGH_VOLTAGE_SET = 0xEDBA
# 0xEDB9,Relay panel high voltage clear,0.01,un16,V
RELAY_PANEL_HIGH_VOLTAGE_CLEAR = 0xEDB9
# 0x100A Relay minimum enabled time 1 un16 minute
RELAY_MINIMUM_ENABLED_TIME = 0x100A
# Lighting controller timer
# Timer events 0..5,-,un32,-
TIMER_EVENT_0 = 0xEDA0
# 0xEDA7,Mid-point shift,1,sn16,min
MID_POINT_SHIFT = 0xEDA7
# 0xED9B,Gradual dim speed,1,un8,s
GRADUAL_DIM_SPEED = 0xED9B
# 0xED9A,Panel voltage night,0.01,un16,V
PANEL_VOLTAGE_NIGHT = 0xED9A
# 0xED99,Panel voltage day,0.01,un16,V
PANEL_VOLTAGE_DAY = 0xED99
# 0xED96,Sunset delay,1,un16,min
SUNSET_DELAY = 0xED96
# 0xED97,Sunrise delay,1,un16,min
SUNRISE_DELAY = 0xED97
# 0xED90,AES Timer,1,un16,min
AES_TIMER = 0xED90
# 0x2030,Solar activity,-,un8,0=dark, 1=light
SOLAR_ACTIVITY = 0x2030
# 0x2031 Time-of-day 1 un16 min, 0=mid-night
TIME_OF_DAY = 0x2031
# VE.Direct port functions
# 0xED9E,TX Port operation mode,-,un8,-
TX_PORT_OPERATION_MODE = 0xED9E
# 0xED98 RX Port operation mode - un8 -
RX_PORT_OPERATION_MODE = 0xED98
# Restore factory defaults
# 0x0004,Restore default
RESTORE_DEFAULT = 0x0004
# History data
# 0x1030,Clear history
CLEAR_HISTORY = 0x1030
# 0x104F,Total history
TOTAL_HISTORY = 0x104F
# Items
HISTORY_0 = 0x1050
# Pluggable display settings
# 0x0400,Display backlight mode,,,un8
DISPLAY_BACKLIGHT_MODE = 0x0400
# 0x0401,Display backlight intensity,,un8,
DISPLAY_BACKLIGHT_INTENSITY = 0x0401
# 0x0402,Display scroll text speed,,un8,
DISPLAY_SCROLL_TEXT_SPEED = 0x0402
# 0x0403,Display setup lock (*2),,,un8
DISPLAY_SETUP_LOCK = 0x0403
# 0x0404,Display temperature unit (*2),,,un8
DISPLAY_TEMPERATURE_UNIT = 0x0404
# Remote control registers
# 0x2000,Charge algorithm version,-,un8,-
CHARGE_ALGORITHM_VERSION = 0x2000
# 0x2001,Charge voltage set-point,0.01,un16,V
CHARGE_VOLTAGE_SET_POINT = 0x2001
# 0x2002,Battery voltage sense,0.01,un16,V
BATTERY_VOLTAGE_SENSE = 0x2002
# 0x2003,Battery temperature sense,0.01,sn16,°C
BATTERY_TEMPERATURE_SENSE = 0x2003
# 0x2004,Remote command,-,un8,-
REMOTE_COMMAND = 0x2004
# 0x2007,Charge state elapsed time,1,un32,ms
CHARGE_STATE_ELAPSED_TIME = 0x2007
# 0x2008,Absorption time,0.01,un16,hours
ABSORPTION_TIME = 0x2008
# 0x2009,Error code,-,un8,-
ERROR_CODE = 0x2009
# 0x200A,Battery charge current,0.001,sn32,A
BATTERY_CHARGE_CURRENT = 0x200A
# 0x200B,Battery idle voltage,0.01,un16,V
BATTERY_IDLE_VOLTAGE = 0x200B
# 0x200C,Device state,-,un8,-
REMOTE_DEVICE_STATE = 0x200C
# 0x200D,Network info,-,un8,-
NETWORK_INFO = 0x200D
# 0x200E,Network mode,-,un8,-
NETWORK_MODE = 0x200E
# 0x200F,Network status register,-,un8,-
NETWORK_STATUS_REGISTER = 0x200F
# 0x2013,Total charge current,0.001,sn32,A
TOTAL_CHARGE_CURRENT = 0x2013
# 0x2014,Charge current percentage,1,un8,%
CHARGE_CURRENT_PERCENTAGE = 0x2014
# 0x2015,Charge current limit,0.1,un16,A
CHARGE_CURRENT_LIMIT = 0x2015
# 0x2018,Manual equalisation pending,-,un8,-
MANUAL_EQUALISATION_PENDING = 0x2018
# 0x2027,Total DC input power,0.01,un32,W
TOTAL_DC_INPUT_POWER = 0x2027
# Product information registers
PRODUCT_ID = Register(0x0100, 'Product ID', 'xHx')
GROUP_ID = Register(0x0104, 'Group ID', 'B')
SERIAL_NUMBER = Register(0x010A, 'Serial number', 'S')
MODEL_NAME = Register(0x010B, 'Model name', 'S')
CAPABILITIES = Register(0x0140, 'Capabilities', 'I', None, None,
schema.Capabilities)
# Generic device control registers
DEVICE_MODE = Register(0x0200, 'Device mode', 'B')
DEVICE_STATE = Register(0x0201, 'Device state', 'B', None, None, schema.State)
REMOTE_CONTROL_USED = Register(0x0202, 'Remote control used', 'I')
DEVICE_OFF_REASON = Register(0x0205, 'Device off reason', 'B')
DEVICE_OFF_REASON_EXTENDED = Register(0x0207, 'Device off reason', 'I')
# Battery settings registers
BATTERYSAFE_MODE = Register(0xEDFF, '')
ADAPTIVE_MODE = Register(0xEDFE, 'Adaptive mode', 'B', None, None, bool)
AUTOMATIC_EQUALISATION_MODE = Register(0xEDFD, 'Automatic equalisation mode',
'B')
BATTERY_BULK_TIME_LIMIT = Register(0xEDFC, 'Battery bulk time limit', 'H',
None, 0.01, _ureg.hour)
BATTERY_ABSORPTION_TIME_LIMIT = Register(0xEDFB,
'Battery absorption time limit', 'H',
None, 0.01, _ureg.hour)
BATTERY_ABSORPTION_VOLTAGE = Register(0xEDF7, 'Battery absorption voltage',
'H', None, 0.01, _ureg.volt)
BATTERY_FLOAT_VOLTAGE = Register(0xEDF6, 'Battery float voltage', 'H', None,
0.01, _ureg.volt)
BATTERY_EQUALISATION_VOLTAGE = Register(0xEDF4, 'Battery equalisation voltage',
'H', None, 0.01, _ureg.volt)
BATTERY_TEMP_COMPENSATION = Register(0xEDF2, 'Battery temp. compensation', 'h',
None, 0.01, 'mV/K')
BATTERY_TYPE = Register(0XEDF1, 'Battery type', 'B')
BATTERY_MAXIMUM_CURRENT = Register(0xEDF0, 'Battery maximum current', 'H',
None, 0.1, _ureg.ampere)
BATTERY_VOLTAGE = Register(0xEDEF, 'Battery voltage', 'B', None, None,
_ureg.volt)
BATTERY_VOLTAGE_SETTING = Register(0xEDEA, 'Battery voltage setting', 'B',
None, None, _ureg.volt)
BMS_PRESENT = Register(0xEDE8, 'BMS present', 'B', None, 1, bool)
TAIL_CURRENT = Register(0xEDE7, 'Tail current', 'H', None, 0.1, '')
LOW_TEMPERATURE_CHARGE_CURRENT = Register(0xEDE6,
'Low temperature charge current',
'H', None, 0.1, _ureg.ampere)
AUTO_EQUALISE_STOP_ON_VOLTAGE = Register(0xEDE5,
'Auto equalise stop on voltage', 'B',
None, 1, bool)
EQUALISATION_CURRENT_LEVEL = Register(0xEDE4, 'Equalisation current level',
'B')
EQUALISATION_DURATION = Register(0xEDE3, 'Equalisation duration', 'H', None,
0.01, _ureg.hour)
RE_BULK_VOLTAGE_OFFSET = Register(0xED2E, 'Re-bulk voltage offset', 'H', None,
0.01, _ureg.volt)
BATTERY_LOW_TEMPERATURE_LEVEL = Register(0xEDE0,
'Battery low temperature level', 'h',
None, 0.01, _ureg.degC)
VOLTAGE_COMPENSATION = Register(0xEDCA, 'Voltage compensation', 'H', None,
0.01, _ureg.volt)
# Charger data registers
BATTERY_TEMPERATURE = Register(0xEDEC, 'Battery temperature', 'H', None, 0.01,
_ureg.kelvin)
CHARGER_MAXIMUM_CURRENT = Register(0xEDDF, 'Charger maximum current', 'H',
None, 0.01, _ureg.ampere)
SYSTEM_YIELD = Register(0xEDDD, 'System yield', 'I', None, 10, _ureg.watt_hour)
USER_YIELD = Register(0xEDDC, 'User yield', 'I', None, 10, _ureg.watt_hour)
CHARGER_INTERNAL_TEMPERATURE = Register(0xEDDB, 'Charger internal temperature',
'h', None, 0.01, _ureg.degC)
CHARGER_ERROR_CODE = Register(0xEDDA, 'Charger error code', 'B', None, None,
schema.Err)
CHARGER_CURRENT = Register(0xEDD7, 'Charger current', 'H', None, 0.1,
_ureg.ampere)
CHARGER_VOLTAGE = Register(0xEDD5, 'Charger voltage', 'H', None, 0.01,
_ureg.volt)
ADDITIONAL_CHARGER_STATE_INFO = Register(0xEDD4,
'Additional charger state info', 'B',
None, None,
schema.AdditionalChargerState)
YIELD_TODAY = Register(0xEDD3, 'Yield today', 'H', None, 10, _ureg.watt_hour)
MAXIMUM_POWER_TODAY = Register(0xEDD2, 'Maximum power today', 'H', None, 1,
_ureg.watt)
YIELD_YESTERDAY = Register(0xEDD1, 'Yield yesterday', 'H', None, 10,
_ureg.watt_hour)
MAXIMUM_POWER_YESTERDAY = Register(0xEDD0, 'Maximum power yesterday', 'H',
None, 1, _ureg.watt)
VOLTAGE_SETTINGS_RANGE = Register(
0xEDCE,
'Voltage settings range',
'H',
None,
)
HISTORY_VERSION = Register(0xEDCD, 'History version', 'B')
STREETLIGHT_VERSION = Register(0xEDCC, 'Streetlight version', 'B')
ADJUSTABLE_VOLTAGE_MINIMUM = Register(0x2211, 'Adjustable voltage minimum',
'H', None, 0.01, _ureg.volt)
ADJUSTABLE_VOLTAGE_MAXIMUM = Register(0x2212, 'Adjustable voltage maximum',
'H', None, 0.01, _ureg.volt)
# Solar panel data registers
PANEL_POWER = Register(0xEDBC, 'Panel power', 'I', None, 0.01, _ureg.watt)
PANEL_VOLTAGE = Register(0xEDBB, 'Panel voltage', 'H', None, 0.01, _ureg.volt)
PANEL_CURRENT = Register(0xEDBD, 'Panel current', 'H', None, 0.1, _ureg.ampere)
PANEL_MAXIMUM_VOLTAGE = Register(0xEDB8, 'Panel maximum voltage', 'H', None,
0.01, _ureg.volt)
TRACKER_MODE = Register(0xEDB3, 'Tracker mode', 'B', None, None,
schema.TrackerMode)
# Load output data/settings registers
LOAD_CURRENT = Register(0xEDAD, 'Load current', 'H', None, 0.1, _ureg.ampere)
# TODO: spec says H.
LOAD_OFFSET_VOLTAGE = Register(0xEDAC, 'Load offset voltage', 'B', None, 0.01,
_ureg.volt)
LOAD_OUTPUT_CONTROL = Register(0xEDAB, 'Load output control', 'B', None, None,
schema.LoadOutputControl)
LOAD_OUTPUT_VOLTAGE = Register(0xEDA9, 'Load output voltage', 'H', None, 0.01,
_ureg.volt)
LOAD_OUTPUT_STATE = Register(0xEDA8, 'Load output state', 'B')
LOAD_SWITCH_HIGH_LEVEL = Register(0xED9D, 'Load switch high level', 'H', None,
0.01, _ureg.volt)
LOAD_SWITCH_LOW_LEVEL = Register(0xED9C, 'Load switch low level', 'H', None,
0.01, _ureg.volt)
LOAD_OUTPUT_OFF_REASON = Register(0xED91, 'Load output off reason', 'B', None,
None, schema.OffReason)
LOAD_AES_TIMER = Register(0xED90, 'Load AES timer', 'H', None, 1, _ureg.minute)
# Relay settings registers
RELAY_OPERATION_MODE = Register(0xEDD9, 'Relay operation mode', 'B', None,
None, schema.RelayMode)
RELAY_BATTERY_LOW_VOLTAGE_SET = Register(0x0350,
'Relay battery low voltage set', 'H',
None, 0.01, _ureg.volt)
RELAY_BATTERY_LOW_VOLTAGE_CLEAR = Register(0x0351,
'Relay battery low voltage clear',
'H', None, 0.01, _ureg.volt)
RELAY_BATTERY_HIGH_VOLTAGE_SET = Register(0x0352,
'Relay battery high voltage set',
'H', None, 0.01, _ureg.volt)
RELAY_BATTERY_HIGH_VOLTAGE_CLEAR = Register(
0x0353, 'Relay battery high voltage clear', 'H', None, 0.01, _ureg.volt)
RELAY_PANEL_HIGH_VOLTAGE_SET = Register(0xEDBA, 'Relay panel high voltage set',
'H', None, 0.01, _ureg.volt)
RELAY_PANEL_HIGH_VOLTAGE_CLEAR = Register(0xEDB9,
'Relay panel high voltage clear',
'H', None, 0.01, _ureg.volt)
RELAY_MINIMUM_ENABLED_TIME = Register(0x100A, 'Relay minimum enabled time',
'H', None, 1, _ureg.minute)
# Lighting controller timer
# TIMER_EVENT_0 = Register(, 'Timer events None, 0..5,un32,-,-')
MID_POINT_SHIFT = Register(0xEDA7, 'Mid-point shift', 'h', None, 60,
_ureg.second)
GRADUAL_DIM_SPEED = Register(0xED9B, 'Gradual dim speed', 'B', None, 1,
_ureg.second)
PANEL_VOLTAGE_NIGHT = Register(0xED9A, 'Panel voltage night', 'H', None, 0.01,
_ureg.volt)
PANEL_VOLTAGE_DAY = Register(0xED99, 'Panel voltage day', 'H', None, 0.01,
_ureg.volt)
SUNSET_DELAY = Register(0xED96, 'Sunset delay', 'H', None, 60, _ureg.second)
SUNRISE_DELAY = Register(0xED97, 'Sunrise delay', 'H', None, 60, _ureg.second)
AES_TIMER = Register(0xED90, 'AES Timer', 'H', None, 60, _ureg.second)
SOLAR_ACTIVITY = Register(0x2030, 'Solar activity', 'B', None, 1, bool)
TIME_OF_DAY = Register(0x2031, 'Time-of-day', 'H')
# VE.Direct port functions
TX_PORT_OPERATION_MODE = Register(0xED9E, 'TX Port operation mode', 'B', None,
None, schema.TXPortMode)
RX_PORT_OPERATION_MODE = Register(0xED98, 'RX Port operation mode', 'B', None,
None, schema.RXPortMode)
# Restore factory defaults
RESTORE_DEFAULT = Register(0x0004, 'Restore default')
# History data
# CLEAR_HISTORY = Register(0x1030, 'Total history')
# HISTORY_0 = Register(0, 'Clear history')
# TOTAL_HISTORY = Register(0x104F, 'Items')
# Pluggable display settings
DISPLAY_BACKLIGHT_MODE = Register(0x0400, 'Display backlight mode,,,un8')
DISPLAY_BACKLIGHT_INTENSITY = Register(0x0401, 'Display backlight intensity',
'B')
DISPLAY_SCROLL_TEXT_SPEED = Register(0x0402, 'Display scroll text speed', 'B',
None)
DISPLAY_SETUP_LOCK = Register(0x0403, 'Display setup lock', 'B')
DISPLAY_TEMPERATURE_UNIT = Register(0x0404, 'Display temperature unit', 'B',
None)
# Remote control registers
CHARGE_ALGORITHM_VERSION = Register(0x2000, 'Charge algorithm version', 'B',
None)
CHARGE_VOLTAGE_SET_POINT = Register(0x2001, 'Charge voltage set-point', 'H',
None, 0.01, _ureg.volt)
BATTERY_VOLTAGE_SENSE = Register(0x2002, 'Battery voltage sense', 'H', None,
0.01, _ureg.volt)
BATTERY_TEMPERATURE_SENSE = Register(0x2003, 'Battery temperature sense', 'h',
None, 0.01, _ureg.degC)
REMOTE_COMMAND = Register(0x2004, 'Remote command', 'B')
CHARGE_STATE_ELAPSED_TIME = Register(0x2007, 'Charge state elapsed time', 'I',
None, 1e-3, _ureg.second)
ABSORPTION_TIME = Register(0x2008, 'Absorption time', 'H', None, 0.01,
_ureg.hour)
ERROR_CODE = Register(0x2009, 'Error code', 'B')
BATTERY_CHARGE_CURRENT = Register(0x200A, 'Battery charge current', 'i', None,
0.001, _ureg.ampere)
BATTERY_IDLE_VOLTAGE = Register(0x200B, 'Battery idle voltage', 'H', None,
0.01, _ureg.volt)
REMOTE_DEVICE_STATE = Register(0x200C, 'Device state', 'B')
NETWORK_INFO = Register(0x200D, 'Network info', 'B')
NETWORK_MODE = Register(0x200E, 'Network mode', 'B')
NETWORK_STATUS_REGISTER = Register(0x200F, 'Network status register', 'B',
None)
TOTAL_CHARGE_CURRENT = Register(0x2013, 'Total charge current', 'i', None,
0.001, _ureg.ampere)
CHARGE_CURRENT_PERCENTAGE = Register(0x2014, 'Charge current percentage', 'B')
CHARGE_CURRENT_LIMIT = Register(0x2015, 'Charge current limit', 'H', None, 0.1,
_ureg.ampere)
MANUAL_EQUALISATION_PENDING = Register(0x2018, 'Manual equalisation pending',
'B')
TOTAL_DC_INPUT_POWER = Register(0x2027, 'Total DC input power', 'I', None,
0.01, _ureg.watt)
REGISTERS = {
ID(r.id): r
for r in globals().values() if isinstance(r, Register)
}
for register in REGISTERS.values():
assert register.mode is None, register
if register.scale is not None:
assert isinstance(register.scale, (float, int)), register
@dataclass
class Information:
product_id: int = Field(PRODUCT_ID)
# group_id: float = Field(GROUP_ID)
serial_number: str = Field(SERIAL_NUMBER)
model_name: str = Field(MODEL_NAME)
capabilities: schema.Capabilities = Field(CAPABILITIES)
@dataclass
class Generic:
mode: int = Field(DEVICE_MODE)
state: schema.State = Field(DEVICE_STATE)
@dataclass
class Settings:
absorption_time_limit: float = Field(BATTERY_ABSORPTION_TIME_LIMIT)
absorption_voltage: float = Field(BATTERY_ABSORPTION_VOLTAGE)
float_voltage: float = Field(BATTERY_FLOAT_VOLTAGE)
equalisation_voltage: float = Field(BATTERY_EQUALISATION_VOLTAGE)
temp_compensation: float = Field(BATTERY_TEMP_COMPENSATION)
type: int = Field(BATTERY_TYPE)
maximum_current: float = Field(BATTERY_MAXIMUM_CURRENT)
voltage: float = Field(BATTERY_VOLTAGE)
voltage_setting: float = Field(BATTERY_VOLTAGE_SETTING)
@dataclass
class Charger:
maximum_current: float = Field(CHARGER_MAXIMUM_CURRENT)
system_yield: float = Field(SYSTEM_YIELD)
user_yield: float = Field(USER_YIELD)
internal_temperature: float = Field(CHARGER_INTERNAL_TEMPERATURE)
error_code: schema.Err = Field(CHARGER_ERROR_CODE)
current: float = Field(CHARGER_CURRENT)
voltage: float = Field(CHARGER_VOLTAGE)
additional_state_info: schema.AdditionalChargerState = Field(
ADDITIONAL_CHARGER_STATE_INFO)
yield_today: float = Field(YIELD_TODAY)
maximum_power_today: float = Field(MAXIMUM_POWER_TODAY)
yield_yesterday: float = Field(YIELD_YESTERDAY)
maximum_power_yesterday: float = Field(MAXIMUM_POWER_YESTERDAY)
@dataclass
class Solar:
power: float = Field(PANEL_POWER)
voltage: float = Field(PANEL_VOLTAGE)
maximum_voltage: float = Field(PANEL_MAXIMUM_VOLTAGE)
tracker_mode: schema.TrackerMode = Field(TRACKER_MODE)
@dataclass
class Load:
current: float = Field(LOAD_CURRENT)
offset_voltage: float = Field(LOAD_OFFSET_VOLTAGE)
output_control: float = Field(LOAD_OUTPUT_CONTROL)
output_state: float = Field(LOAD_OUTPUT_STATE)
switch_high_level: float = Field(LOAD_SWITCH_HIGH_LEVEL)
mswitch_low_level: float = Field(LOAD_SWITCH_LOW_LEVEL)
GROUPS = (Information, Generic, Settings, Charger, Solar, Load)
PRODUCT_IDS = {
0x203: 'BMV-700',
0x204: 'BMV-702',
0x205: 'BMV-700H',
0x0300: 'BlueSolar MPPT 70|15',
0xA040: 'BlueSolar MPPT 75|50',
0xA041: 'BlueSolar MPPT 150|35',
0xA042: 'BlueSolar MPPT 75|15',
0xA043: 'BlueSolar MPPT 100|15',
0xA044: 'BlueSolar MPPT 100|30',
0xA045: 'BlueSolar MPPT 100|50',
0xA046: 'BlueSolar MPPT 150|70',
0xA047: 'BlueSolar MPPT 150|100',
0xA049: 'BlueSolar MPPT 100|50 rev2',
0xA04A: 'BlueSolar MPPT 100|30 rev2',
0xA04B: 'BlueSolar MPPT 150|35 rev2',
0xA04C: 'BlueSolar MPPT 75|10',
0xA04D: 'BlueSolar MPPT 150|45',
0xA04E: 'BlueSolar MPPT 150|60',
0xA04F: 'BlueSolar MPPT 150|85',
0xA050: 'SmartSolar MPPT 250|100',
0xA051: 'SmartSolar MPPT 150|100',
0xA052: 'SmartSolar MPPT 150|85',
0xA053: 'SmartSolar MPPT 75|15',
0xA054: 'SmartSolar MPPT 75|10',
0xA055: 'SmartSolar MPPT 100|15',
0xA056: 'SmartSolar MPPT 100|30',
0xA057: 'SmartSolar MPPT 100|50',
0xA058: 'SmartSolar MPPT 150|35',
0xA059: 'SmartSolar MPPT 150|100 rev2',
0xA05A: 'SmartSolar MPPT 150|85 rev2',
0xA05B: 'SmartSolar MPPT 250|70',
0xA05C: 'SmartSolar MPPT 250|85',
0xA05D: 'SmartSolar MPPT 250|60',
0xA05E: 'SmartSolar MPPT 250|45',
0xA05F: 'SmartSolar MPPT 100|20',
0xA060: 'SmartSolar MPPT 100|20 48V',
0xA061: 'SmartSolar MPPT 150|45',
0xA062: 'SmartSolar MPPT 150|60',
0xA063: 'SmartSolar MPPT 150|70',
0xA064: 'SmartSolar MPPT 250|85 rev2',
0xA065: 'SmartSolar MPPT 250|100 rev2',
0xA066: 'BlueSolar MPPT 100|20',
0xA067: 'BlueSolar MPPT 100|20 48V',
0xA068: 'SmartSolar MPPT 250|60 rev2',
0xA069: 'SmartSolar MPPT 250|70 rev2',
0xA06A: 'SmartSolar MPPT 150|45 rev2',
0xA06B: 'SmartSolar MPPT 150|60 rev2',
0xA06C: 'SmartSolar MPPT 150|70 rev2',
0xA06D: 'SmartSolar MPPT 150|85 rev3',
0xA06E: 'SmartSolar MPPT 150|100 rev3',
0xA06F: 'BlueSolar MPPT 150|45 rev2',
0xA070: 'BlueSolar MPPT 150|60 rev2',
0xA071: 'BlueSolar MPPT 150|70 rev2',
0xA102: 'SmartSolar MPPT VE.Can 150/70',
0xA103: 'SmartSolar MPPT VE.Can 150/45',
0xA104: 'SmartSolar MPPT VE.Can 150/60',
0xA105: 'SmartSolar MPPT VE.Can 150/85',
0xA106: 'SmartSolar MPPT VE.Can 150/100',
0xA107: 'SmartSolar MPPT VE.Can 250/45',
0xA108: 'SmartSolar MPPT VE.Can 250/60',
0xA109: 'SmartSolar MPPT VE.Can 250/70',
0xA10A: 'SmartSolar MPPT VE.Can 250/85',
0xA10B: 'SmartSolar MPPT VE.Can 250/100',
0xA10C: 'SmartSolar MPPT VE.Can 150/70 rev2',
0xA10D: 'SmartSolar MPPT VE.Can 150/85 rev2',
0xA10E: 'SmartSolar MPPT VE.Can 150/100 rev2',
0xA10F: 'BlueSolar MPPT VE.Can 150/100',
0xA112: 'BlueSolar MPPT VE.Can 250/70',
0xA113: 'BlueSolar MPPT VE.Can 250/100',
0xA114: 'SmartSolar MPPT VE.Can 250/70 rev2',
0xA115: 'SmartSolar MPPT VE.Can 250/100 rev2',
0xA116: 'SmartSolar MPPT VE.Can 250/85 rev2',
0xA201: 'Phoenix Inverter 12V 250VA 230V',
0xA202: 'Phoenix Inverter 24V 250VA 230V',
0xA204: 'Phoenix Inverter 48V 250VA 230V',
0xA211: 'Phoenix Inverter 12V 375VA 230V',
0xA212: 'Phoenix Inverter 24V 375VA 230V',
0xA214: 'Phoenix Inverter 48V 375VA 230V',
0xA221: 'Phoenix Inverter 12V 500VA 230V',
0xA222: 'Phoenix Inverter 24V 500VA 230V',
0xA224: 'Phoenix Inverter 48V 500VA 230V',
0xA231: 'Phoenix Inverter 12V 250VA 230V',
0xA232: 'Phoenix Inverter 24V 250VA 230V',
0xA234: 'Phoenix Inverter 48V 250VA 230V',
0xA239: 'Phoenix Inverter 12V 250VA 120V',
0xA23A: 'Phoenix Inverter 24V 250VA 120V',
0xA23C: 'Phoenix Inverter 48V 250VA 120V',
0xA241: 'Phoenix Inverter 12V 375VA 230V',
0xA242: 'Phoenix Inverter 24V 375VA 230V',
0xA244: 'Phoenix Inverter 48V 375VA 230V',
0xA249: 'Phoenix Inverter 12V 375VA 120V',
0xA24A: 'Phoenix Inverter 24V 375VA 120V',
0xA24C: 'Phoenix Inverter 48V 375VA 120V',
0xA251: 'Phoenix Inverter 12V 500VA 230V',
0xA252: 'Phoenix Inverter 24V 500VA 230V',
0xA254: 'Phoenix Inverter 48V 500VA 230V',
0xA259: 'Phoenix Inverter 12V 500VA 120V',
0xA25A: 'Phoenix Inverter 24V 500VA 120V',
0xA25C: 'Phoenix Inverter 48V 500VA 120V',
0xA261: 'Phoenix Inverter 12V 800VA 230V',
0xA262: 'Phoenix Inverter 24V 800VA 230V',
0xA264: 'Phoenix Inverter 48V 800VA 230V',
0xA269: 'Phoenix Inverter 12V 800VA 120V',
0xA26A: 'Phoenix Inverter 24V 800VA 120V',
0xA26C: 'Phoenix Inverter 48V 800VA 120V',
0xA271: 'Phoenix Inverter 12V 1200VA 230V',
0xA272: 'Phoenix Inverter 24V 1200VA 230V',
0xA274: 'Phoenix Inverter 48V 1200VA 230V',
0xA279: 'Phoenix Inverter 12V 1200VA 120V',
0xA27A: 'Phoenix Inverter 24V 1200VA 120V',
0xA27C: 'Phoenix Inverter 48V 1200VA 120V',
0xA281: 'Phoenix Inverter 12V 1600VA 230V',
0xA282: 'Phoenix Inverter 24V 1600VA 230V',
0xA284: 'Phoenix Inverter 48V 1600VA 230V',
0xA291: 'Phoenix Inverter 12V 2000VA 230V',
0xA292: 'Phoenix Inverter 24V 2000VA 230V',
0xA294: 'Phoenix Inverter 48V 2000VA 230V',
0xA2A1: 'Phoenix Inverter 12V 3000VA 230V',
0xA2A2: 'Phoenix Inverter 24V 3000VA 230V',
0xA2A4: 'Phoenix Inverter 48V 3000VA 230V',
0xA340: 'Phoenix Smart IP43 Charger 12|50 (1+1)',
0xA341: 'Phoenix Smart IP43 Charger 12|50 (3)',
0xA342: 'Phoenix Smart IP43 Charger 24|25 (1+1)',
0xA343: 'Phoenix Smart IP43 Charger 24|25 (3)',
0xA344: 'Phoenix Smart IP43 Charger 12|30 (1+1)',
0xA345: 'Phoenix Smart IP43 Charger 12|30 (3)',
0xA346: 'Phoenix Smart IP43 Charger 24|16 (1+1)',
0xA347: 'Phoenix Smart IP43 Charger 24|16 (3)',
}

429
vedirect/phoenix.py Normal file
View file

@ -0,0 +1,429 @@
from dataclasses import dataclass
import dataclasses
from typing import Optional
import enum
import pint
from .hex import Register, Mode, Field
_ureg = pint.get_application_registry()
@dataclass
class Product:
id: int
name: str
remark: Optional[str] = None
PRODUCT_IDS = (
Product(0xA201, 'Phoenix Inverter 12V 250VA 230Vac', 'obsolete (32k)'),
Product(0xA202, 'Phoenix Inverter 24V 250VA 230Vac', 'obsolete (32k)'),
Product(0xA204, 'Phoenix Inverter 48V 250VA 230Vac', 'obsolete (32k)'),
Product(0xA211, 'Phoenix Inverter 12V 375VA 230Vac', 'obsolete (32k)'),
Product(0xA212, 'Phoenix Inverter 24V 375VA 230Vac', 'obsolete (32k)'),
Product(0xA214, 'Phoenix Inverter 48V 375VA 230Vac', 'obsolete (32k)'),
Product(0xA221, 'Phoenix Inverter 12V 500VA 230Vac', 'obsolete (32k)'),
Product(0xA222, 'Phoenix Inverter 24V 500VA 230Vac', 'obsolete (32k)'),
Product(0xA224, 'Phoenix Inverter 48V 500VA 230Vac', 'obsolete (32k)'),
Product(0xA231, 'Phoenix Inverter 12V 250VA 230Vac 64k'),
Product(0xA232, 'Phoenix Inverter 24V 250VA 230Vac 64k'),
Product(0xA234, 'Phoenix Inverter 48V 250VA 230Vac 64k'),
Product(0xA239, 'Phoenix Inverter 12V 250VA 120Vac 64k'),
Product(0xA23A, 'Phoenix Inverter 24V 250VA 120Vac 64k'),
Product(0xA23C, 'Phoenix Inverter 48V 250VA 120Vac 64k'),
Product(0xA241, 'Phoenix Inverter 12V 375VA 230Vac 64k'),
Product(0xA242, 'Phoenix Inverter 24V 375VA 230Vac 64k'),
Product(0xA244, 'Phoenix Inverter 48V 375VA 230Vac 64k'),
Product(0xA249, 'Phoenix Inverter 12V 375VA 120Vac 64k'),
Product(0xA24A, 'Phoenix Inverter 24V 375VA 120Vac 64k'),
Product(0xA24C, 'Phoenix Inverter 48V 375VA 120Vac 64k'),
Product(0xA251, 'Phoenix Inverter 12V 500VA 230Vac 64k'),
Product(0xA252, 'Phoenix Inverter 24V 500VA 230Vac 64k'),
Product(0xA254, 'Phoenix Inverter 48V 500VA 230Vac 64k'),
Product(0xA259, 'Phoenix Inverter 12V 500VA 120Vac 64k'),
Product(0xA25A, 'Phoenix Inverter 24V 500VA 120Vac 64k'),
Product(0xA25C, 'Phoenix Inverter 48V 500VA 120Vac 64k'),
Product(0xA261, 'Phoenix Inverter 12V 800VA 230Vac 64k'),
Product(0xA262, 'Phoenix Inverter 24V 800VA 230Vac 64k'),
Product(0xA264, 'Phoenix Inverter 48V 800VA 230Vac 64k'),
Product(0xA269, 'Phoenix Inverter 12V 800VA 120Vac 64k'),
Product(0xA26A, 'Phoenix Inverter 24V 800VA 120Vac 64k'),
Product(0xA26C, 'Phoenix Inverter 48V 800VA 120Vac 64k'),
Product(0xA271, 'Phoenix Inverter 12V 1200VA 230Vac 64k'),
Product(0xA272, 'Phoenix Inverter 24V 1200VA 230Vac 64k'),
Product(0xA274, 'Phoenix Inverter 48V 1200VA 230Vac 64k'),
Product(0xA279, 'Phoenix Inverter 12V 1200VA 120Vac 64k'),
Product(0xA27A, 'Phoenix Inverter 24V 1200VA 120Vac 64k'),
Product(0xA27C, 'Phoenix Inverter 48V 1200VA 120Vac 64k'),
Product(0xA281, 'Phoenix Inverter Smart 12V 1600VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA282, 'Phoenix Inverter Smart 24V 1600VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA284, 'Phoenix Inverter Smart 48V 1600VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA291, 'Phoenix Inverter Smart 12V 2000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA292, 'Phoenix Inverter Smart 24V 2000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA294, 'Phoenix Inverter Smart 48V 2000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA2A1, 'Phoenix Inverter Smart 12V 3000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA2A2, 'Phoenix Inverter Smart 24V 3000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA2A4, 'Phoenix Inverter Smart 48V 3000VA 230Vac 64k',
'Smart, integrated Bluetooth'),
Product(0xA2E1, 'Phoenix Inverter 12V 800VA 230Vac 64k HS',
'redesign (replaces A261)'),
Product(0xA2E2, 'Phoenix Inverter 24V 800VA 230Vac 64k HS',
'redesign (replaces A262)'),
Product(0xA2E4, 'Phoenix Inverter 48V 800VA 230Vac 64k HS',
'redesign (replaces A264)'),
Product(0xA2E9, 'Phoenix Inverter 12V 800VA 120Vac 64k HS',
'redesign (replaces A269)'),
Product(0xA2EA, 'Phoenix Inverter 24V 800VA 120Vac 64k HS',
'redesign (replaces A26A)'),
Product(0xA2EC, 'Phoenix Inverter 48V 800VA 120Vac 64k HS',
'redesign (replaces A26C)'),
Product(0xA2F1, 'Phoenix Inverter 12V 1200VA 230Vac 64k HS',
'redesign (replaces A271)'),
Product(0xA2F2, 'Phoenix Inverter 24V 1200VA 230Vac 64k HS',
'redesign (replaces A272)'),
Product(0xA2F4, 'Phoenix Inverter 48V 1200VA 230Vac 64k HS',
'redesign (replaces A274)'),
Product(0xA2F9, 'Phoenix Inverter 12V 1200VA 120Vac 64k HS',
'redesign (replaces A279)'),
Product(0xA2FA, 'Phoenix Inverter 24V 1200VA 120Vac 64k HS',
'redesign (replaces A27A)'),
Product(0xA2FC, 'Phoenix Inverter 48V 1200VA 120Vac 64k HS',
'redesign (replaces A27C)'),
)
# NVM registers
INV_NVM_COMMAND = Register(0xEB99, 'INV_NVM_COMMAND', 'B', Mode.W)
RESTORE_DEFAULT = Register(0x0004, 'RESTORE_DEFAULT', '-', Mode.W)
# class NVMCommand(enum.IntEnum):
# # 1,NvmSave,Save current user settings to NVM,,,,
# # 2,NvmRevert,Cancel modified settings.,,,,
# # ,,Load most recent saved user settings.,,,,
# # 3,NvmBackup,Undo last save. Load second last time saved settings.,,,,
# # 4,NvmDefault,Load the factory default values.,,,,
# Product information registers
class Capabilities(enum.IntFlag):
# 8,Remote input available
REMOTE_INPUT_AVAILABLE = 1 << 8
# 17,Build in user-relay available
# TYPO
BUILD_IN_USER_RELAY_AVAILABLE = 1 << 17
# 28,Support of device hibernation
SUPPORT_OF_DEVICE_HIBERNATION = 1 << 28
# 29,Improved load current measurement
IMPROVED_LOAD_CURRENT_MEASUREMENT = 1 << 29
PRODUCT_ID = Register(0x0100, 'PRODUCT_ID', 'xHx', Mode.R)
PRODUCT_REVISION = Register(0x0101, 'PRODUCT_REVISION', 'xH', Mode.R, None,
None)
APP_VER = Register(0x0102, 'APP_VER', 'xH', Mode.R)
SERIAL_NUMBER = Register(0x010A, 'SERIAL_NUMBER', 'S', Mode.R)
MODEL_NAME = Register(0x010B, 'MODEL_NAME', 'S', Mode.R)
AC_OUT_RATED_POWER = Register(0x2203, 'AC_OUT_RATED_POWER', 'h', Mode.R, 1,
'VA')
# Typo.
CAPABILITIES = Register(0x0140, 'CAPABILITIES', 'I', Mode.R, None,
Capabilities)
CAPABILITIES_BLE = Register(0x0150, 'CAPABILITIES_BLE', 'I', Mode.RW)
AC_OUT_NOM_VOLTAGE = Register(0x2202, 'AC_OUT_NOM_VOLTAGE', 'B', Mode.R, 1,
'V')
BAT_VOLTAGE = Register(0xEDEF, 'BAT_VOLTAGE', 'B', Mode.R, 1, 'V')
# # Generic device status registers
class DeviceState(enum.IntEnum):
# Off,Not inverting. When due to a protection the inverter will
# automatically start again when the cause is solved.
OFF = 0
# Low Power,Eco load search active
LOW_POWER = 1
# Fault,Not inverting due to a fatal active protection. A turn OFF-ON cycle
# is required to enable the device again.
FAULT = 2
# Inverting,Normal operating
INVERTING = 9
class DeviceOffReason(enum.IntFlag):
NONE = 0
# 0,No input power (will also cause a battery alarm),
NO_INPUT_POWER = 1 << 0
# 2,Soft power button or SW controlled (VE.Direct or Bluetooth),
SOFT_POWER_BUTTON_OR_SW_CONTROLLED = 1 << 2
# 3,HW remote input connector,
HW_REMOTE_INPUT_CONNECTOR = 1 << 3
# 4,Internal reason (see alarm reason for more info),
INTERNAL_REASON = 1 << 4
# 5,"PayGo, out of credit, need token",
PAYGO_OUT_OF_CREDIT_NEED_TOKEN = 1 << 5
class WarningReason(enum.IntFlag):
NONE = 0
# 0,Low battery voltage,
LOW_BATTERY_VOLTAGE = 1 << 0
# 1,High battery voltage,
HIGH_BATTERY_VOLTAGE = 1 << 1
# 5,Low temperature,
LOW_TEMPERATURE = 1 << 5
# 6,High temperature,
HIGH_TEMPERATURE = 1 << 6
# 8,Overload,
OVERLOAD = 1 << 8
# 9,Poor DC connection,
POOR_DC_CONNECTION = 1 << 9
# 10,Low AC-output voltage,
LOW_AC_OUTPUT_VOLTAGE = 1 << 10
# 11,High AC-output voltage,
HIGH_AC_OUTPUT_VOLTAGE = 1 << 11
DEVICE_STATE = Register(0x0201, 'DEVICE_STATE', 'B', Mode.R, None, DeviceState)
DEVICE_OFF_REASON = Register(0x0207, 'DEVICE_OFF_REASON', 'I', Mode.R, None,
DeviceOffReason)
WARNING_REASON = Register(0x031C, 'WARNING_REASON', 'H', Mode.R, None,
WarningReason)
ALARM_REASON = Register(0x031E, 'ALARM_REASON', 'H', Mode.R, None,
WarningReason)
# Generic device control registers
class DeviceMode(enum.IntEnum):
# 2,Inverter On,,,,,
INVERTER_ON = 2
# 3,Device On (multi compliant),1),,,,
DEVICE_ON = 3
# 4,Device Off,VE.Direct is still enabled,,,,
DEVICE_OFF = 4
# 5,Eco mode,,,,,
ECO_MODE = 5
# 0xFD,Hibernate,VE.Direct is affected 2),,,,
HIBERNATE = 0xFD
BLE_MODE = Register(0x0090, 'BLE_MODE', 'B', Mode.RW)
DEVICE_MODE = Register(0x0200, 'DEVICE_MODE', 'B', Mode.RW, None, DeviceMode)
SETTINGS_CHANGED = Register(0xEC41, 'SETTINGS_CHANGED', 'I', Mode.RW)
# Inverter operation registers
HISTORY_TIME = Register(0x1040, 'HISTORY_TIME', 'I', Mode.R, 1, _ureg.second)
HISTORY_ENERGY = Register(0x1041, 'HISTORY_ENERGY', 'I', Mode.R, 0.01 * 1000,
_ureg.watthour)
AC_OUT_CURRENT = Register(0x2201, 'AC_OUT_CURRENT', 'h', Mode.R, 0.1,
_ureg.amp)
AC_OUT_VOLTAGE = Register(0x2200, 'AC_OUT_VOLTAGE', 'h', Mode.R, 0.01,
_ureg.volt)
AC_OUT_APPARENT_POWER = Register(0x2205, 'AC_OUT_APPARENT_POWER', 'i', Mode.R,
1, _ureg.watt)
INV_LOOP_GET_IINV = Register(0xEB4E, 'INV_LOOP_GET_IINV', 'h', Mode.R, 0.001,
_ureg.amp)
DC_CHANNEL1_VOLTAGE = Register(0xED8D, 'DC_CHANNEL1_VOLTAGE', 'h', Mode.R,
0.01, _ureg.volt)
# User AC-out control registers
AC_OUT_VOLTAGE_SETPOINT = Register(0x0230, 'AC_OUT_VOLTAGE_SETPOINT', 'H',
Mode.W, 0.01, _ureg.volt)
AC_OUT_VOLTAGE_SETPOINT_MIN = Register(0x0231, 'AC_OUT_VOLTAGE_SETPOINT_MIN',
'H', Mode.R, 0.01, _ureg.volt)
AC_OUT_VOLTAGE_SETPOINT_MAX = Register(0x0232, 'AC_OUT_VOLTAGE_SETPOINT_MAX',
'H', Mode.R, 0.01, _ureg.volt)
AC_LOAD_SENSE_POWER_THRESHOLD = Register(0x2206,
'AC_LOAD_SENSE_POWER_THRESHOLD', 'H',
Mode.W, None, _ureg.watt)
AC_LOAD_SENSE_POWER_CLEAR = Register(0x2207, 'AC_LOAD_SENSE_POWER_CLEAR', 'H',
Mode.W, None, _ureg.watt)
INV_WAVE_SET50HZ_NOT60HZ = Register(0xEB03, 'INV_WAVE_SET50HZ_NOT60HZ', 'B',
Mode.W)
INV_OPER_ECO_MODE_INV_MIN = Register(0xEB04, 'INV_OPER_ECO_MODE_INV_MIN', 'h',
Mode.W, 0.001, _ureg.A)
INV_OPER_ECO_MODE_RETRY_TIME = Register(0xEB06, 'INV_OPER_ECO_MODE_RETRY_TIME',
'B', Mode.W, 0.25, _ureg.second)
INV_OPER_ECO_LOAD_DETECT_PERIODS = Register(
0xEB10, 'INV_OPER_ECO_LOAD_DETECT_PERIODS', 'B', Mode.W, 0.02, _ureg.hertz)
# User battery control registers
SHUTDOWN_LOW_VOLTAGE_SET = Register(0x2210, 'SHUTDOWN_LOW_VOLTAGE_SET', 'H',
Mode.W, 0.01, _ureg.volt)
ALARM_LOW_VOLTAGE_SET = Register(0x0320, 'ALARM_LOW_VOLTAGE_SET', 'H', Mode.W,
0.01, _ureg.volt)
ALARM_LOW_VOLTAGE_CLEAR = Register(0x0321, 'ALARM_LOW_VOLTAGE_CLEAR', 'H',
Mode.W, 0.01, _ureg.volt)
VOLTAGE_RANGE_MIN = Register(0x2211, 'VOLTAGE_RANGE_MIN', 'H', Mode.R, 0.01,
_ureg.volt)
VOLTAGE_RANGE_MAX = Register(0x2212, 'VOLTAGE_RANGE_MAX', 'H', Mode.R, 0.01,
_ureg.volt)
# Datasheet says H.
INV_PROT_UBAT_DYN_CUTOFF_ENABLE = Register(0xEBBA,
'INV_PROT_UBAT_DYN_CUTOFF_ENABLE',
'B', Mode.W)
INV_PROT_UBAT_DYN_CUTOFF_FACTOR = Register(0xEBB7,
'INV_PROT_UBAT_DYN_CUTOFF_FACTOR',
'H', Mode.W)
INV_PROT_UBAT_DYN_CUTOFF_FACTOR2000 = Register(
0xEBB5, 'INV_PROT_UBAT_DYN_CUTOFF_FACTOR2000', 'H', Mode.W)
INV_PROT_UBAT_DYN_CUTOFF_FACTOR250 = Register(
0xEBB3, 'INV_PROT_UBAT_DYN_CUTOFF_FACTOR250', 'H', Mode.W)
INV_PROT_UBAT_DYN_CUTOFF_FACTOR5 = Register(
0xEBB2, 'INV_PROT_UBAT_DYN_CUTOFF_FACTOR5', 'H', Mode.W)
INV_PROT_UBAT_DYN_CUTOFF_VOLTAGE = Register(
0xEBB1, 'INV_PROT_UBAT_DYN_CUTOFF_VOLTAGE', 'H', Mode.R, 0.001, _ureg.volt)
# Relay control registers
class RelayMode(enum.IntEnum):
# Normal operation. On during normal operation (warnings are ignored).
NORMAL_OPERATION = 4
# Warnings and alarms. Off when a warning or alarm is active (inverter on).
WARNINGS_AND_ALARMS = 0
# Battery low. Off when a low battery warning or alarm is active.
BATTERY_LOW = 5
# External fan. On when the internal fan is on.
EXTERNAL_FAN = 6
# Disabled relay. Always Off.
DISABLED_RELAY = 3
# Remote. Controlled by writing to RELAY_CONTROL (0x034E).
REMOTE = 2
RELAY_CONTROL = Register(0x034E, 'RELAY_CONTROL', 'B', Mode.RW)
RELAY_MODE = Register(0x034F, 'RELAY_MODE', 'B', Mode.W, None, RelayMode)
class NVMCommand(enum.IntEnum):
NONE = 0
SAVE = 1
REVERT = 2
LOAD_BACKUP = 3
LOAD_DEFAULTS = 4
INV_NVM_COMMAND = Register(0xEB99, 'INV_NVM_COMMAND', 'B', Mode.W, None,
NVMCommand)
ID = int
REGISTERS = {
ID(r.id): r
for r in globals().values() if isinstance(r, Register)
}
@dataclass
class NVM:
inv_nvm_command: int = Field(INV_NVM_COMMAND)
@dataclass
class Information:
product_id: int = Field(PRODUCT_ID)
product_revision: int = Field(PRODUCT_REVISION)
app_ver: int = Field(APP_VER)
serial_number: str = Field(SERIAL_NUMBER)
model_name: str = Field(MODEL_NAME)
ac_out_rated_power: float = Field(AC_OUT_RATED_POWER)
capabilities: Capabilities = Field(CAPABILITIES)
# capabilities_ble: float = Field(CAPABILITIES_BLE)
ac_out_nom_voltage: float = Field(AC_OUT_NOM_VOLTAGE)
bat_voltage: float = Field(BAT_VOLTAGE)
@dataclass
class Status:
device_state: DeviceState = Field(DEVICE_STATE)
device_off_reason: DeviceOffReason = Field(DEVICE_OFF_REASON)
warning_reason: WarningReason = Field(WARNING_REASON)
alarm_reason: int = Field(ALARM_REASON)
@dataclass
class DeviceControl:
# ble_mode: float = Field(BLE_MODE)
device_mode: DeviceMode = Field(DEVICE_MODE)
settings_changed: int = Field(SETTINGS_CHANGED)
@dataclass
class Inverter:
history_time: float = Field(HISTORY_TIME)
history_energy: float = Field(HISTORY_ENERGY)
ac_out_current: float = Field(AC_OUT_CURRENT)
ac_out_voltage: float = Field(AC_OUT_VOLTAGE)
# ac_out_apparent_power: float = Field(AC_OUT_APPARENT_POWER)
inv_loop_get_iinv: float = Field(INV_LOOP_GET_IINV)
dc_channel1_voltage: float = Field(DC_CHANNEL1_VOLTAGE)
@dataclass
class ACControl:
ac_out_voltage_setpoint: float = Field(AC_OUT_VOLTAGE_SETPOINT)
ac_out_voltage_setpoint_min: float = Field(AC_OUT_VOLTAGE_SETPOINT_MIN)
ac_out_voltage_setpoint_max: float = Field(AC_OUT_VOLTAGE_SETPOINT_MAX)
# ac_load_sense_power_threshold: float = Field(
# AC_LOAD_SENSE_POWER_THRESHOLD)
# ac_load_sense_power_clear: float = Field(
# AC_LOAD_SENSE_POWER_CLEAR)
inv_wave_set50hz_not60hz: int = Field(INV_WAVE_SET50HZ_NOT60HZ)
inv_oper_eco_mode_inv_min: float = Field(INV_OPER_ECO_MODE_INV_MIN)
inv_oper_eco_mode_retry_time: float = Field(INV_OPER_ECO_MODE_RETRY_TIME)
inv_oper_eco_load_detect_periods: float = Field(
INV_OPER_ECO_LOAD_DETECT_PERIODS)
@dataclass
class BatteryControl:
shutdown_low_voltage_set: float = Field(SHUTDOWN_LOW_VOLTAGE_SET)
alarm_low_voltage_set: float = Field(ALARM_LOW_VOLTAGE_SET)
alarm_low_voltage_clear: float = Field(ALARM_LOW_VOLTAGE_CLEAR)
voltage_range_min: float = Field(VOLTAGE_RANGE_MIN)
voltage_range_max: float = Field(VOLTAGE_RANGE_MAX)
inv_prot_ubat_dyn_cutoff_enable: int = Field(
INV_PROT_UBAT_DYN_CUTOFF_ENABLE)
inv_prot_ubat_dyn_cutoff_factor: int = Field(
INV_PROT_UBAT_DYN_CUTOFF_FACTOR)
inv_prot_ubat_dyn_cutoff_factor2000: int = Field(
INV_PROT_UBAT_DYN_CUTOFF_FACTOR2000)
inv_prot_ubat_dyn_cutoff_factor250: int = Field(
INV_PROT_UBAT_DYN_CUTOFF_FACTOR250)
inv_prot_ubat_dyn_cutoff_factor5: int = Field(
INV_PROT_UBAT_DYN_CUTOFF_FACTOR5)
inv_prot_ubat_dyn_cutoff_voltage: float = Field(
INV_PROT_UBAT_DYN_CUTOFF_VOLTAGE)
@dataclass
class RelayControl:
relay_control: float = Field(RELAY_CONTROL)
relay_mode: float = Field(RELAY_MODE)
GROUPS = (
Information,
Status,
DeviceControl,
Inverter,
ACControl,
BatteryControl,
NVM,
)
for group in GROUPS:
for field in dataclasses.fields(group):
r = field.metadata.get('vedirect.Register', None)
assert r is not None
if field.type == int:
assert r.scale is None, r
elif field.type == float:
assert r.scale is not None, r

324
vedirect/schema.py Normal file
View file

@ -0,0 +1,324 @@
from dataclasses import dataclass
import enum
from typing import Callable, Union
import pint
_ureg = pint.UnitRegistry()
W = _ureg.watt
V = _ureg.volt
A = _ureg.amp
C = float # _ureg.degree_Celsius
K = _ureg.kelvin
Hours = _ureg.hour
mVK = float # _ureg.mvolt * 1e-3 / _ureg.kelvin
Percent = float
kWh = float
Minute = _ureg.minute
Unknown = int
class State(enum.IntEnum):
"""State is the state of operation. Sent in the `CS` field."""
# Off
OFF = 0
# Low power
LOW_POWER = 1
# Fault
FAULT = 2
# Bulk
BULK = 3
# Absorption
ABSORPTION = 4
# Float
FLOAT = 5
# Storage
STORAGE = 6
# Equalize (manual)
EQUALIZE_MANUAL = 7
# Inverting
INVERTING = 9
# Power supply
POWER_SUPPLY = 11
# Starting-up
STARTING_UP = 245
# Repeated absorption
REPEATED_ABSORPTION = 246
# Auto equalize / Recondition
AUTO_EQUALIZE_RECONDITION = 247
# BatterySafe
BATTERYSAFE = 248
# External Control
EXTERNAL_CONTROL = 252
class Capabilities(enum.IntFlag):
LOAD_OUTPUT_PRESENT = 1 << 0
ROTARY_ENCODER_PRESENT = 1 << 1
HISTORY_SUPPORT = 1 << 2
BATTERYSAFE_MODE = 1 << 3
ADAPTIVE_MODE = 1 << 4
MANUAL_EQUALISE = 1 << 5
AUTOMATIC_EQUALISE = 1 << 6
STORAGE_MODE = 1 << 7
REMOTE_ON_OFF_VIA_RX_PIN = 1 << 8
SOLAR_TIMER_STREETLIGHTING = 1 << 9
ALTERNATIVE_VEDIRECT_TX_PIN_FUNCTION = 1 << 10
USER_DEFINED_LOAD_SWITCH = 1 << 11
LOAD_CURRENT_IN_TEXT_PROTOCOL = 1 << 12
PANEL_CURRENT = 1 << 13
BMS_SUPPORT = 1 << 14
EXTERNAL_CONTROL_SUPPORT = 1 << 15
PARALLEL_CHARGING_SUPPORT = 1 << 16
ALARM_RELAY = 1 << 17
ALTERNATIVE_VEDIRECT_RX_PIN_FUNCTION = 1 << 18
VIRTUAL_LOAD_OUTPUT = 1 << 19
VIRTUAL_RELAY = 1 << 20
PLUGIN_DISPLAY_SUPPORT = 1 << 21
UNDEFINED_24 = 1 << 24
LOAD_AUTOMATIC_ENERGY_SELECTOR = 1 << 25
BATTERY_TEST = 1 << 26
PAYGO_SUPPORT = 1 << 27
class Err(enum.IntEnum):
"""Err is the error code of the device. Sent in the `ERR` field."""
# No error
NO_ERROR = 0
# Battery voltage too high
BATTERY_VOLTAGE_TOO_HIGH = 2
# Charger temperature too high
CHARGER_TEMPERATURE_TOO_HIGH = 17
# Charger over current
CHARGER_OVER_CURRENT = 18
# Charger current reversed
CHARGER_CURRENT_REVERSED = 19
# Bulk time limit exceeded
BULK_TIME_LIMIT_EXCEEDED = 20
# Current sensor issue (sensor bias/sensor broken)
CURRENT_SENSOR_ISSUE = 21
# Terminals overheated
TERMINALS_OVERHEATED = 26
# Input voltage too high (solar panel)
INPUT_VOLTAGE_TOO_HIGH_SOLAR_PANEL = 33
# Input current too high (solar panel)
INPUT_CURRENT_TOO_HIGH_SOLAR_PANEL = 34
# Input shutdown (due to excessive battery voltage)
INPUT_SHUTDOWN = 38
# Factory calibration data lost
FACTORY_CALIBRATION_DATA_LOST = 116
# Invalid/incompatible firmware
INVALID_OR_INCOMPATIBLE_FIRMWARE = 117
# User settings invalid
USER_SETTINGS_INVALID = 119
class OffReason(enum.IntFlag):
NO_INPUT_POWER = 1 << 0
RESERVED = 1 << 1
SOFT_POWER_SWITCH = 1 << 2
REMOTE_INPUT = 1 << 3
INTERNAL_REASOn = 1 << 4
PAY_AS_YOU_GO_OUT_OF_CREDIT = 1 << 5
BMS_SHUTDOWN = 1 << 6
RESERVED_2 = 1 << 7
class AdditionalChargerState(enum.IntFlag):
SAFE_MODE_ACTIVE = 1 << 0
AUTOMATIC_EQUALISATION_ACTIVE = 1 << 1
TEMPERATURE_DIMMING_ACTIVeE = 1 << 4
INPUT_CURRENT_DIMMING_ACTIVE = 1 << 6
class LoadOutputOffReason(enum.IntFlag):
BATTERY_LOW = 1 << 0
SHORT_CIRCUIT = 1 << 1
TIMER_PROGRAM = 1 << 2
REMOTE_INPUT = 1 << 3
PAY_AS_YOU_GO_OUT_OF_CREDIT = 1 << 4
RESERVED = 1 << 5
RESERVED_2 = 1 << 6
DEVICE_STARTING_UP = 1 << 7
class LoadOutputControl(enum.IntEnum):
LOAD_OUTPUT_OFF = 0
AUTOMATIC_CONTROL_BATTERYLIFE = 1
ALTERNATIVE_CONTROL_1 = 2
ALTERNATIVE_CONTROL_2 = 3
LOAD_OUTPUT_ON = 4
USER_DEFINED_SETTINGS_1 = 5
USER_DEFINED_SETTINGS_2 = 6
AUTOMATIC_ENERGY_SELECTOR = 7
class RelayMode(enum.IntEnum):
RELAY_ALWAYS_OFF = 0
PANEL_VOLTAGE_HIGH = 1
INTERNAL_TEMPERATURE_HIGH = 2
BATTERY_VOLTAGE_TOO_LOW = 3
EQUALISATION_ACTIVE = 4
ERROR_CONDITION_PRESENT = 5
INTERNAL_TEMPERATURE_LOW = 6
BATTERY_VOLTAGE_TOO_HIGH = 7
CHARGER_IN_FLOAT_OR_STORAGE = 8
DAY_DETECTION = 9
LOAD_CONTROL = 10
class TXPortMode(enum.IntEnum):
NORMAL_VEDIRECT_COMMUNICATION = 0
PULSE_ON_HARVEST = 1
LIGHTING_CONTROL_PWM_NORMAL = 2
LIGHTING_CONTROL_PWM_INVERTED = 3
VIRTUAL_LOAD_OUTPUT = 4
class RXPortMode(enum.IntEnum):
REMOTE_ON_OFF = 0
LOAD_OUTPUT_CONFIGURATION = 1
LOAD_OUTPUT_ON_OFF_REMOTE_CONTROL_INVERTED = 2
LOAD_OUTPUT_ON_OFF_REMOTE_CONTROL = 3
class TrackerMode(enum.IntEnum):
OFF = 0
LIMITED = 1
MPPT = 2
# Schema
@dataclass
class Register:
"""Defines a single register on the controller."""
command: int
scale: Union[float, Callable, None]
size: Union[str, Callable]
class Group:
"""Tags a group of registers."""
pass
@dataclass
class Product(Group):
id: int = Register(0x0100, None, 'I')
group_id: int = Register(0x0104, None, 'B')
serial_number: str = Register(0x010A, None, str)
model_name: str = Register(0x010B, None, str)
capabilities: Capabilities = Register(0x0140, Capabilities, 'I')
@dataclass
class Device(Group):
mode: int = Register(0x200, None, 'B')
state: State = Register(0x201, State, 'B')
remote_control_used: Unknown = Register(0x202, None, 'I')
off_reason: OffReason = Register(0x0207, OffReason, 'I')
@dataclass
class Load(Group):
current: A = Register(0xEDAD, 0.1, 'H')
offset_voltage: V = Register(0xEDAC, 0.01, 'B') # Spec says H.
output_control: LoadOutputControl = Register(0xEDAB, LoadOutputControl,
'B')
output_voltage: V = Register(0xEDA9, 0.01, 'H')
output_state: Unknown = Register(0xEDA8, None, 'B')
switch_high_level: V = Register(0xED9D, 0.01, 'H')
switch_low_level: V = Register(0xED9C, 0.01, 'H')
output_off_reason: LoadOutputOffReason = Register(0xED91,
LoadOutputOffReason, 'B')
aes_timer: Minute = Register(0xED90, 1, 'H')
@dataclass
class Relay(Group):
relay_operation_mode: RelayMode = Register(0xEDD9, RelayMode, 'B')
battery_low_voltage_set: V = Register(0x0350, 0.01, 'H')
battery_low_voltage_clear: V = Register(0x0351, 0.01, 'H')
battery_high_voltage_set: V = Register(0x0352, 0.01, 'H')
battery_high_voltage_clear: V = Register(0x0353, 0.01, 'H')
panel_high_voltage_set: V = Register(0xEDBA, 0.01, 'H')
panel_high_voltage_clear: V = Register(0xEDB9, 0.01, 'H')
minimum_enabled_time: Minute = Register(0x100A, 1, 'H')
@dataclass
class Charger(Group):
battery_temperature: K = Register(0xEDEC, 0.01, 'H')
maximum_current: A = Register(0xEDDF, 0.01, 'H')
system_yield: kWh = Register(0xEDDD, 0.01, 'I')
user_yield: kWh = Register(0xEDDC, 0.01, 'I')
internal_temperature: C = Register(0xEDDB, 0.01, 'h')
error_code: Err = Register(0xEDDA, Err, 'B')
current: A = Register(0xEDD7, 0.1, 'H')
voltage: V = Register(0xEDD5, 0.01, 'H')
additional_state_info: AdditionalChargerState = Register(
0xEDD4, AdditionalChargerState, 'B')
yield_today: kWh = Register(0xEDD3, 0.01, 'H')
maximum_power_today: W = Register(0xEDD2, 1, 'H')
yield_yesterday: kWh = Register(0xEDD1, 0.01, 'H')
maximum_power_yesterday: W = Register(0xEDD0, 1, 'H')
voltage_settings: Unknown = Register(0xEDCE, None, 'H')
history_version: Unknown = Register(0xEDCD, None, 'B')
streetlight_version: Unknown = Register(0xEDCC, None, 'B')
adjustable_voltage_minimum: V = Register(0x2211, 0.01, 'H')
@dataclass
class Panel(Group):
power: W = Register(0xEDBC, 0.01, 'I')
voltage: V = Register(0xEDBB, 0.01, 'H')
current: A = Register(0xEDBD, 0.1, 'H')
maximum_voltage: V = Register(0xEDB8, 0.01, 'H')
tracker_mode: Unknown = Register(0xEDB3, None, 'B')
@dataclass
class Battery(Group):
batterysafe_mode: bool = Register(0xEDFF, None, 'B')
adaptive_mode: bool = Register(0xEDFE, None, 'B')
automatic_equalisation_mode: int = Register(0xEDFD, None, 'B')
bulk_time_limit: Hours = Register(0xEDFC, 0.01, 'H')
absorption_time_limit: Hours = Register(0xEDFB, 0.01, 'H')
absorption_voltage: V = Register(0xEDF7, 0.01, 'H')
float_voltage: V = Register(0xEDF6, 0.01, 'H')
equalisation_voltage: V = Register(0xEDF4, 0.01, 'H')
temp_compensation: mVK = Register(0xEDF2, 0.01, 'h')
type: int = Register(0xedf1, 1, 'b')
maximum_current: A = Register(0xEDF0, 0.1, 'H')
voltage: V = Register(0xEDEF, 1, 'B')
temperature: K = Register(0xEDEC, 0.01, 'H')
voltage_setting: V = Register(0xEDEA, 1, 'B')
bms_present: bool = Register(0xEDE8, None, 'B')
tail_current: A = Register(0xEDE7, 0.1, 'H')
low_temperature_charge_current: A = Register(0xEDE6, 0.1, 'H')
auto_equalise_stop_on_voltage: bool = Register(0xEDE5, None, 'B')
equalisation_current_level: Percent = Register(0xEDE4, 1, 'B')
equalisation_duration: Hours = Register(0xEDE3, 0.01, 'H')
re_bulk_voltage_offset: V = Register(0xED2E, 0.01, 'H')
low_temperature_level: C = Register(0xEDE0, 0.01, 'h')
voltage_compensation: V = Register(0xEDCA, 0.01, 'H')
@dataclass
class VEDirectPort(Group):
tx_port_mode: TXPortMode = Register(0xED9E, TXPortMode, 'B')
rx_port_mode: RXPortMode = Register(0xED98, RXPortMode, 'B')
@dataclass
class Registers:
"""The registers on the device."""
product: Product = Product()
device: Device = Device()
charger: Charger = Charger()
load: Load = Load()
panel: Panel = Panel()
battery: Battery = Battery()
relay: Relay = Relay()
vedirect_port: VEDirectPort = VEDirectPort()

View file

@ -50,12 +50,12 @@ class _Source:
return ch
def _get_value(label: str, value: bytearray) -> object:
def _get_value(label: str, value_bytes: bytearray) -> object:
"""Parses the value in a label specific way."""
if label == _CHECKSUM:
return value[0]
return value_bytes[0]
value = value.decode()
value = value_bytes.decode()
try:
if label not in defs.FIELD_MAP:
return int(value)