Compare commits
27 commits
Author | SHA1 | Date | |
---|---|---|---|
Michael Hope | 23c0a49a26 | ||
Michael Hope | 66cb355392 | ||
Michael Hope | 0d26752550 | ||
Michael Hope | 9a36e59d38 | ||
Michael Hope | e14e5afa34 | ||
Michael Hope | c682c172d8 | ||
Michael Hope | dda687d25b | ||
Michael Hope | 659546a35b | ||
Michael Hope | 235f6b26df | ||
Michael Hope | 8c7bb58c6b | ||
Michael Hope | 950548a9c3 | ||
Michael Hope | 32fde540f8 | ||
Michael Hope | f0e797f2d6 | ||
Michael Hope | 59ddf1a2c1 | ||
Michael Hope | b521c25dd9 | ||
Michael Hope | be80f12825 | ||
Michael Hope | 83db956dfd | ||
Michael Hope | 2469ebaed0 | ||
Michael Hope | 8bc6764988 | ||
Michael Hope | 414a761918 | ||
Michael Hope | f960699d2d | ||
Michael Hope | 0f299386d9 | ||
Michael Hope | 945a1c5a5f | ||
Michael Hope | c179cbc7c8 | ||
Michael Hope | 29f60a58f1 | ||
Michael Hope | d74b2ec66b | ||
Michael Hope | b8057275a8 |
27
.drone.yml
Normal file
27
.drone.yml
Normal 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
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
.eggs
|
||||
.mypy*
|
||||
.vscode
|
||||
build
|
||||
dis
|
||||
*.pyc
|
||||
*.egg-info
|
||||
*.tar.*
|
7
requirements.txt
Normal file
7
requirements.txt
Normal 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
|
7
setup.py
7
setup.py
|
@ -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
|
||||
|
|
242
vedirect/cli.py
242
vedirect/cli.py
|
@ -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
259
vedirect/hex.py
Normal 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
694
vedirect/mppt.py
Normal 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
429
vedirect/phoenix.py
Normal 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
324
vedirect/schema.py
Normal 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()
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue