Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0.16e #362

Merged
merged 26 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .pylintrc

This file was deleted.

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# MPP-Solar Device Python Package #

__BREAKING CHANGE - command separator changed to `#`__
__BREAKING CHANGES__
- minimum python supported 3.10 for version >=0.16.0
- command separator changed to `#`


Python package with reference library of commands (and responses)
designed to get information from inverters and other solar inverters and power devices
Expand Down
7 changes: 5 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ test: tests/*.py
coverage html

t: tests/*.py
python3 -m unittest -f

tv: tests/*.py
python3 -m unittest -f -v

pypi:
Expand All @@ -26,9 +29,9 @@ docker-powermon-dev-up:
docker compose -f docker-compose.development.yaml up --build

m-build:
docker-compose -f docker-compose.dev-min.yaml up --build
docker compose -f docker-compose.dev-min.yaml up --build

m-run:
docker-compose -f docker-compose.dev-min.yaml run mppsolar mpp-solar -p test -c QID -D
docker compose -f docker-compose.dev-min.yaml run mppsolar mpp-solar -p test -c QID -D
# docker-compose -f docker-compose.dev-min.yaml run mppsolar -p test -c QID

4 changes: 4 additions & 0 deletions mppsolar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def main():
parser.add_argument("--getDeviceId", action="store_true", help="Generate Device ID")

parser.add_argument("-v", "--version", action="store_true", help="Display the version")
parser.add_argument("--getVersion", action="store_true", help="Output the software version via the supplied output")
parser.add_argument(
"-D",
"--debug",
Expand Down Expand Up @@ -386,6 +387,9 @@ def main():
elif args.getDeviceId:
# use get_settings helper
commands.append("get_device_id")
elif args.getVersion:
# use get_version helper
commands.append("get_version")
elif args.command is None:
# run the command
commands.append("")
Expand Down
10 changes: 10 additions & 0 deletions mppsolar/devices/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from abc import ABC

from mppsolar.version import __version__ # noqa: F401
from mppsolar.helpers import get_kwargs
from mppsolar.inout import get_port
from mppsolar.protocols import get_protocol
Expand Down Expand Up @@ -69,6 +70,8 @@ def run_command(self, command) -> dict:
return self.get_settings()
if command == "get_device_id":
return self.get_device_id()
if command == "get_version":
return self.get_version()

if not command:
command = self._protocol.DEFAULT_COMMAND
Expand Down Expand Up @@ -158,3 +161,10 @@ def get_device_id(self) -> dict:
"_command_description": "Generate a device id",
"DeviceID": ["getDeviceId not supported for this protocol", ""],
}

def get_version(self) -> dict:
return {
"_command": "Get Version",
"_command_description": "Output the mpp-solar software version",
"MPP-Solar Software Version": [__version__, ""],
}
16 changes: 10 additions & 6 deletions mppsolar/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,21 @@ def get_device_class(device_type=None):
return device_class


def getMaxLen(d):
def getMaxLen(data, index=0):
_maxLen = 0
for i in d:
if type(i) == list:
i = i[0]
if len(i) > _maxLen:
_maxLen = len(i)
for item in data:
if type(item) == list:
item = item[index]
if type(item) == float or type(item) == int:
item = str(item)
if len(item) > _maxLen:
_maxLen = len(item)
return _maxLen


def pad(text, length):
if type(text) == float or type(text) == int:
text = str(text)
if len(text) > length:
return text
return text.ljust(length, " ")
119 changes: 119 additions & 0 deletions mppsolar/outputs/boxdraw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import logging
import re

from .baseoutput import baseoutput
from ..helpers import get_kwargs, key_wanted, pad, getMaxLen

log = logging.getLogger("boxdraw")


class boxdraw(baseoutput):
def __str__(self):
return "outputs the results to standard out in a table formatted with line art boxes"

def __init__(self, *args, **kwargs) -> None:
log.debug(f"processor.boxdraw __init__ kwargs {kwargs}")

def printHeader(command, description):
pass

def output(self, *args, **kwargs):
log.info("Using output processor: boxdraw")
log.debug(f"kwargs {kwargs}")
data = get_kwargs(kwargs, "data")
if data is None:
return

# check if config supplied
config = get_kwargs(kwargs, "config")
if config is not None:
log.debug(f"config: {config}")
# get formatting info
remove_spaces = config.get("remove_spaces", True)
keep_case = config.get("keep_case", False)
filter = config.get("filter", None)
excl_filter = config.get("excl_filter", None)
else:
# get formatting info
remove_spaces = True
keep_case = get_kwargs(kwargs, "keep_case")
filter = get_kwargs(kwargs, "filter")
excl_filter = get_kwargs(kwargs, "excl_filter")

if filter is not None:
filter = re.compile(filter)
if excl_filter is not None:
excl_filter = re.compile(excl_filter)

# remove raw response
if "raw_response" in data:
data.pop("raw_response")

# build header
if "_command" in data:
command = data.pop("_command")
else:
command = "Unknown command"
if "_command_description" in data:
description = data.pop("_command_description")
else:
description = "No description found"

# build data to display
displayData = {}
for key in data:
_values = data[key]
# remove spaces
if remove_spaces:
key = key.replace(" ", "_")
if not keep_case:
# make lowercase
key = key.lower()
if key_wanted(key, filter, excl_filter):
displayData[key] = _values
log.debug(f"displayData: {displayData}")

# Determine column widths
_pad = 1
# Width of parameter column
width_p = getMaxLen(displayData) + _pad
if width_p < 9 + _pad:
width_p = 9 + _pad
# Width of value column
width_v = getMaxLen(data.values()) + _pad
if width_v < 6 + _pad:
width_v = 6 + _pad
# Width of units column
width_u = getMaxLen(data.values(), 1) + _pad
if width_u < 5 + _pad:
width_u = 5 + _pad
# Total line length
line_length = width_p + width_v + width_u + 7
# Check if command / description line is longer and extend line if needed
cmd_str = f" Command: {command} - {description}"
if line_length < (len(cmd_str) + 7):
line_length = len(cmd_str) + 7
# Check if columns too short and expand units if needed
if (width_p + width_v + width_u + 7) < line_length:
width_u = line_length - (width_p + width_u + 7) - 1

# print header
print("\u2554" + ("\u2550" * (line_length - 2)) + "\u2557")
print(f"\u2551{cmd_str}" + (" " * (line_length - len(cmd_str) - 2)) + "\u2551")

# print separator
print("\u2560" + ("\u2550" * (width_p + 1)) + "\u2564" + ("\u2550" * (width_v + 1)) + "\u2564" + ("\u2550" * (width_u + 1)) + "\u2563")
# print column headings
print(f"\u2551 {pad('Parameter', width_p)}\u2502 {pad('Value', width_v)}\u2502 {pad('Unit', width_u)}\u2551")
# print separator
print("\u255f" + ("\u2500" * (width_p + 1)) + "\u253c" + ("\u2500" * (width_v + 1)) + "\u253c" + ("\u2500" * (width_u + 1)) + "\u2562")

# print data
for key in displayData:
value = displayData[key][0]
unit = displayData[key][1]
print(f"\u2551 {pad(key, width_p)}\u2502 {pad(value, width_v)}\u2502 {pad(unit, width_u)}\u2551")

# print footer
print("\u255a" + ("\u2550" * (width_p + 1)) + "\u2567" + ("\u2550" * (width_v + 1)) + "\u2567" + ("\u2550" * (width_u + 1)) + "\u255d")
print("\n")
4 changes: 4 additions & 0 deletions mppsolar/outputs/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ def output(self, *args, **kwargs):
print(f"{pad(key,maxP+1)}{value:<15}\t{unit:<4}\t{extra}")
else:
print(f"{pad(key,maxP+1)}{value:<15}\t{unit:<4}")

# print footer
print("-" * 80)
print("\n")
23 changes: 23 additions & 0 deletions mppsolar/protocols/pi30.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,29 @@
],
"regex": "PBATMAXDISC([01]\\d\\d)$",
},
"BTA": {
"name": "BTA",
"description": "Calibrate inverter battery voltage",
"help": " -- examples: BTA-01 (reduce inverter reading by 0.05V), BTA+09 (increase inverter reading by 0.45V)",
"type": "SETTER",
"response": [["ack", "Command execution", {"NAK": "Failed", "ACK": "Successful"}]],
"test_responses": [
b"(NAK\x73\x73\r",
b"(ACK\x39\x20\r",
],
"regex": "BTA([-+]0\\d)$",
},
"PSAVE": {
"name": "PSAVE",
"description": "Save EEPROM changes",
"help": " -- examples: PSAVE (save changes to eeprom)",
"type": "SETTER",
"response": [["ack", "Command execution", {"NAK": "Failed", "ACK": "Successful"}]],
"test_responses": [
b"(NAK\x73\x73\r",
b"(ACK\x39\x20\r",
],
},
}
QUERY_COMMANDS = {
"Q1": {
Expand Down
2 changes: 1 addition & 1 deletion mppsolar/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.15.62"
__version__ = "0.16.01"
38 changes: 17 additions & 21 deletions powermon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from mppsolar.version import __version__ # noqa: F401
from powermon.device import Device
from powermon.libs.apicoordinator import ApiCoordinator
from powermon.libs.configurationManager import ConfigurationManager
from powermon.libs.daemon import Daemon
from powermon.libs.mqttbroker import MqttBroker
from powermon.commands.command import Command

# from time import sleep, time
# from powermon.ports import getPortFromConfig
Expand Down Expand Up @@ -78,6 +78,11 @@ def main():
action="store_true",
help="Only loop through config once",
)
parser.add_argument(
"--force",
action="store_true",
help="Force commands to run even if wouldnt be triggered (should only be used with --once)",
)
parser.add_argument(
"-D",
"--debug",
Expand Down Expand Up @@ -123,52 +128,43 @@ def main():
log.info("config: %s" % config)

# build device object (required)
device = Device(config=config.get("device"), commandConfig=config.get("commands"))
device = Device.fromConfig(config=config.get("device"))
log.debug(device)
# add commands to device command list
for commandConfig in config.get("commands"):
command = Command.fromConfig(commandConfig)
if command is not None:
device.add_command(command)
log.info(device)

# build mqtt broker object (optional)
# QUESTION: should mqtt_broker be part of device...
mqtt_broker = MqttBroker(config=config.get("mqttbroker"))
mqtt_broker = MqttBroker.fromConfig(config=config.get("mqttbroker"))
log.info(mqtt_broker)

# build the daemon object (optional)
daemon = Daemon(config=config.get("daemon"))
daemon = Daemon.fromConfig(config=config.get("daemon"))
log.info(daemon)

# build controller
# TODO: follow same pattern as others, eg
# scheduleController = ScheduleController(config=config.get("schedules"), device=, mqtt_broker=)
controller = ConfigurationManager.parseControllerConfig(config, device, mqtt_broker)
log.info(controller)

# build api coordinator
api_coordinator = ApiCoordinator(config=config.get("api"), device=device, mqtt_broker=mqtt_broker, schedule=controller)
api_coordinator = ApiCoordinator.fromConfig(config=config.get("api"), device=device, mqtt_broker=mqtt_broker)
log.info(api_coordinator)

# initialize daemon
daemon.initialize()

# initialize device
device.initialize()
controller.beforeLoop()

# Main working loop
keep_looping = True
controller_looping = True
device_looping = True
try:
while keep_looping:
# tell the daemon we're still working
daemon.watchdog()

# run schedule loop
if controller_looping:
controller_looping = controller.runLoop()
if device_looping:
device_looping = device.runLoop()
# stop looping if neither controller or device runLoops return True
if not controller_looping and not device_looping:
keep_looping = False
keep_looping = device.runLoop(args.force)

# run api coordinator ...
api_coordinator.run()
Expand Down
29 changes: 0 additions & 29 deletions powermon/commands/abstractCommand.py

This file was deleted.

Loading