diff --git a/README.md b/README.md index 26d273c..877b175 100644 --- a/README.md +++ b/README.md @@ -145,10 +145,23 @@ loop = asyncio.get_event_loop() loop.run_until_complete(get_data()) ``` -## libbi support - very early - reads a few values like State of Charge - gets the current status +## Libbi support +Very early and basic support of Libbi. + +- Reads a few values such as State of Charge, DCPV CT +- Battery in and out energy +- Gets and sets the current status +- Change priority of Libbi + +cli examples: +``` +myenergi libbi show +myenergi libbi mode normal +myenergi libbi mode stop +myenergi libbi priority 1 +myenergi libbi energy +``` + ## Credits diff --git a/pymyenergi/VERSION b/pymyenergi/VERSION index 329ee9f..998726e 100644 --- a/pymyenergi/VERSION +++ b/pymyenergi/VERSION @@ -1 +1 @@ -0.0.28 \ No newline at end of file +0.0.29 \ No newline at end of file diff --git a/pymyenergi/base_device.py b/pymyenergi/base_device.py index 32f29c0..0113846 100644 --- a/pymyenergi/base_device.py +++ b/pymyenergi/base_device.py @@ -130,6 +130,11 @@ async def fetch_history_data( "ct4": 0, "ct5": 0, "ct6": 0, + "ive1": 0, + "ivi1": 0, + "bdp1": 0, + "bcp1": 0, + "pvp1": 0 } if resolution == MINUTE: url = f"/cgi-jday-{self.prefix}{self._serialno}-{date_from.year}-{date_from.month}-{date_from.day}-{date_from.hour}-0-{how_long}" @@ -162,10 +167,17 @@ async def fetch_history_data( "generated": round(energy_wh["gep"] / 1000, 2), "grid_import": round(energy_wh["imp"] / 1000, 2), "grid_export": round(energy_wh["exp"] / 1000, 2), + "battery_charge": round(energy_wh["bcp1"] / 1000, 2), + "battery_discharge": round(energy_wh["bdp1"] / 1000, 2), + "inverter_export": round(energy_wh["ive1"] /1000, 2), + "inverter_import": round(energy_wh["ivi1"] /1000, 2), "device_boosted": device_boosted, "device_green": device_green, "device_total": device_boosted + device_green } + if resolution == MINUTE: + return_data["pv_total"] = round(energy_wh["pvp1"] / 1000, 2) + for i in range(6): key = f"ct{i+1}" if hasattr(self, key): diff --git a/pymyenergi/cli.py b/pymyenergi/cli.py index a69ebe8..daadc09 100644 --- a/pymyenergi/cli.py +++ b/pymyenergi/cli.py @@ -80,12 +80,18 @@ async def main(args): sys.exit(f"A mode must be specifed, one of {modes}") await device.set_charge_mode(args.arg[0]) print(f"Charging was set to {args.arg[0].capitalize()}") - elif args.action == "mode" and args.command in [EDDI, LIBBI]: + elif args.action == "mode" and args.command == EDDI: if len(args.arg) < 1 or args.arg[0].capitalize() not in EDDI_MODES: modes = ", ".join(EDDI_MODES) sys.exit(f"A mode must be specifed, one of {modes}") await device.set_operating_mode(args.arg[0]) print(f"Operating mode was set to {args.arg[0].capitalize()}") + elif args.action == "mode" and args.command == LIBBI: + if len(args.arg) < 1 or args.arg[0].capitalize() not in LIBBI_MODES: + modes = ", ".join(LIBBI_MODES) + sys.exit(f"A mode must be specifed, one of {modes}") + await device.set_operating_mode(args.arg[0]) + print(f"Operating mode was set to {args.arg[0].capitalize()}") elif args.action == "mingreen" and args.command == ZAPPI: if len(args.arg) < 1: sys.exit("A minimum green level must be provided") @@ -204,7 +210,7 @@ def cli(): LIBBI, help="use libbi --help for available commands" ) subparser_libbi.add_argument("-s", "--serial", dest="serial", default=None) - subparser_libbi.add_argument("action", choices=["show","mode","priority"]) + subparser_libbi.add_argument("action", choices=["show","mode","priority","energy"]) subparser_libbi.add_argument("arg", nargs="*") args = parser.parse_args() diff --git a/pymyenergi/client.py b/pymyenergi/client.py index e9aa41c..1af62b3 100644 --- a/pymyenergi/client.py +++ b/pymyenergi/client.py @@ -199,6 +199,7 @@ def power_charging(self): def power_battery(self): """Battery total power""" return self._totals.get(CT_BATTERY, 0) + def find_device_name(self, key, default_value): """Find device or site name""" diff --git a/pymyenergi/libbi.py b/pymyenergi/libbi.py index af252e5..5a1969d 100644 --- a/pymyenergi/libbi.py +++ b/pymyenergi/libbi.py @@ -17,10 +17,12 @@ 5:'Charging', 6:'Discharging', 7:'Duration Charging', + 101:'Idle?', 102:'102', - 104:'104' } + 234:'Calibration Charge' } LIBBI_MODES = ["Stopped","Normal"] +LIBBI_MODE_NAMES = ["STOP", "BALANCE"] class Libbi(BaseDevice): """Libbi Client for myenergi API.""" @@ -32,8 +34,6 @@ def __init__(self, connection: Connection, serialno, data={}) -> None: @property def kind(self): return LIBBI - - @property def status(self): @@ -43,6 +43,11 @@ def status(self): return STATES[n] else: return n + + @property + def local_mode(self): + """Get current known status""" + return self._data.get("lmo", 1) @property def prefix(self): @@ -52,7 +57,7 @@ def prefix(self): def ct_keys(self): """Return CT key names that are not none""" keys = {} - for i in range(3): + for i in range(6): ct = getattr(self, f"ct{i+1}") if ct.name_as_key == "ct_none": continue @@ -133,8 +138,31 @@ def battery_size(self): def inverter_size(self): """Inverter size in kwh""" return self._data.get("mic", 0) /1000 - + @property + def grid_import(self): + """Grid import from history data""" + return self.history_data.get("grid_import", 0) + + @property + def grid_export(self): + """Grid export from history data""" + return self.history_data.get("grid_export", 0) + + @property + def battery_charge(self): + """Battery charge from history data""" + return self.history_data.get("battery_charge", 0) + + @property + def battery_discharge(self): + """Battery discharge from history data""" + return self.history_data.get("battery_discharge", 0) + + @property + def generated(self): + """Solar generation from history data""" + return self.history_data.get("generated", 0) @property def prefix(self): @@ -143,15 +171,12 @@ def prefix(self): async def set_operating_mode(self, mode: str): """Stopped or normal mode""" - print(f"set mode") + print("current mode", self._data["lmo"]) mode_int = LIBBI_MODES.index(mode.capitalize()) await self._connection.get( f"/cgi-libbi-mode-{self.prefix}{self._serialno}-{mode_int}" ) - if mode_int == 0: - self._data["sta"] = 0 - else: - self._data["sta"] = 1 + self._data["lmo"] = LIBBI_MODE_NAMES[mode_int] return True async def set_priority(self, priority): @@ -177,9 +202,10 @@ def show(self, short_format=False): ret = ret + f"Battery size: {self.battery_size}kWh\n" ret = ret + f"Inverter size: {self.inverter_size}kWh\n" ret = ret + f"State of Charge: {self.state_of_charge}%\n" - ret = ret + f"Generated: {self.power_generated}W\n" + ret = ret + f"Generating: {self.power_generated}W\n" ret = ret + f"Grid: {self.power_grid}W\n" ret = ret + f"Status : {self.status}\n" + ret = ret + f"Local Mode : {self.local_mode}\n" ret = ret + f"CT 1 {self.ct1.name} {self.ct1.power}W phase {self.ct1.phase}\n" ret = ret + f"CT 2 {self.ct2.name} {self.ct2.power}W phase {self.ct2.phase}\n" ret = ret + f"CT 3 {self.ct3.name} {self.ct3.power}W phase {self.ct3.phase}\n" @@ -188,6 +214,4 @@ def show(self, short_format=False): ret = ret + f"CT 6 {self.ct6.name} {self.ct6.power}W phase {self.ct6.phase}\n" for key in self.ct_keys: ret = ret + f"Energy {key} {self.history_data.get(key, 0)}Wh\n" - - return ret diff --git a/requirements_test.txt b/requirements_test.txt index b8c33bd..dc3702a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ -r requirements_dev.txt -pytest-sugar==0.9.4 -pytest-timeout==1.4.2 +pytest-sugar==0.9.7 +pytest-timeout==2.1.0 pytest-httpx -pytest-xdist==2.2.1 -pytest==6.2.4 +pytest-xdist==3.3.1 +pytest==7.4.2 pytest-asyncio diff --git a/tests/conftest.py b/tests/conftest.py index 16329dc..bd4c7d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,6 +74,15 @@ def harvi_fetch_data_fixture(): yield +@pytest.fixture(name="libbi_fetch_data_fixture") +def libbi_fetch_data_fixture(): + """Mock data from client.fetch_data()""" + with patch( + "pymyenergi.libbi.Libbi.fetch_data", return_value=load_fixture_json("libbi") + ): + yield + + # @pytest.fixture # def eddi_connection_mock(): # with patch("pymyenergi.eddi.Eddi._connection"): diff --git a/tests/fixtures/client.json b/tests/fixtures/client.json index 4627c10..26cf988 100644 --- a/tests/fixtures/client.json +++ b/tests/fixtures/client.json @@ -6,6 +6,7 @@ { "key": "Z17005900", "val": "Test Zappi 2" }, { "key": "H10645200", "val": "Test Harvi 1" }, { "key": "H10644500", "val": "Test Harvi 2" }, + { "key": "L24047164", "val": "Test Libbi 1" }, { "key": "siteName", "val": "Test Site" } ] }, @@ -154,6 +155,51 @@ } ] }, + { + "libbi": [ + { + "sno": 24047164, + "dat": "20-09-2023", + "tim": "14: 53: 03", + "ectp1": -457, + "ectp2": -72, + "ectp3": 0, + "ectt1": "Internal Load", + "ectt2": "Grid", + "ectt3": "None", + "ectp4": 0, + "ectp5": 320, + "ectt4": "None", + "ectt5": "DCPV", + "ectt6": "None", + "dst": 1, + "tz": 0, + "lmo": "BALANCE", + "sta": 6, + "frq": 49.92, + "pri": 1, + "soc": 45, + "isp": "True", + "pha": 1, + "vol": 2350, + "mbc": 20400, + "mic": 5000, + "gen": 320, + "grd": -38, + "div": -457, + "ect1p": 1, + "ect2p": 1, + "ect3p": 1, + "batteryDischargingBoost": "False", + "pvDirectlyConnected": "True", + "g100LockoutState": "NONE", + "cmt": 254, + "fwv": "3702S5.041", + "newAppAvailable": "False", + "newBootloaderAvailable": "False" + } + ] + }, { "asn": "s8.myenergi.net", "fwv": "3401S3077" diff --git a/tests/fixtures/libbi.json b/tests/fixtures/libbi.json new file mode 100644 index 0000000..6b7d9a5 --- /dev/null +++ b/tests/fixtures/libbi.json @@ -0,0 +1,41 @@ +{ + "sno": 24047164, + "dat": "20-09-2023", + "tim": "14: 53: 03", + "ectp1": -457, + "ectp2": -72, + "ectp3": 0, + "ectt1": "Internal Load", + "ectt2": "Grid", + "ectt3": "None", + "ectp4": 0, + "ectp5": 320, + "ectt4": "None", + "ectt5": "DCPV", + "ectt6": "None", + "dst": 1, + "tz": 0, + "lmo": "BALANCE", + "sta": 6, + "frq": 49.92, + "pri": 1, + "soc": 45, + "isp": "True", + "pha": 1, + "vol": 2350, + "mbc": 20400, + "mic": 5000, + "gen": 320, + "grd": -38, + "div": -457, + "ect1p": 1, + "ect2p": 1, + "ect3p": 1, + "batteryDischargingBoost": "False", + "pvDirectlyConnected": "True", + "g100LockoutState": "NONE", + "cmt": 254, + "fwv": "3702S5.041", + "newAppAvailable": "False", + "newBootloaderAvailable": "False" +} \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index 1a5e0c6..f70adb7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,6 +3,7 @@ from pymyenergi.eddi import Eddi from pymyenergi.harvi import Harvi from pymyenergi.zappi import Zappi +from pymyenergi.libbi import Libbi # All test coroutines will be treated as marked. pytestmark = pytest.mark.asyncio @@ -25,7 +26,7 @@ async def test_init_error(error_on_client_fetch_data): async def test_get_all_devices(client_fetch_data_fixture): client = MyenergiClient(conn) devices = await client.get_devices() - assert len(devices) == 5 + assert len(devices) == 6 async def test_get_eddi_devices(client_fetch_data_fixture): @@ -62,3 +63,10 @@ async def test_1p_harvi_eddi_solar_battery(client_1p_zappi_harvi_solar_battery_f assert client.power_battery == 3000 assert client.power_charging == 2000 assert client.consumption_home == 16000 + + +async def test_get_libbi_devices(client_fetch_data_fixture): + client = MyenergiClient(conn) + devices = await client.get_devices("libbi") + assert len(devices) == 1 + assert isinstance(devices[0], Libbi) \ No newline at end of file diff --git a/tests/test_libbi.py b/tests/test_libbi.py new file mode 100644 index 0000000..16c6591 --- /dev/null +++ b/tests/test_libbi.py @@ -0,0 +1,11 @@ +import pytest +from pymyenergi.libbi import Libbi + +pytestmark = pytest.mark.asyncio + + +async def test_refresh(libbi_fetch_data_fixture): + """Test Libbi data""" + libbi = Libbi({}, 24047164) + await libbi.refresh() + assert libbi.serial_number == 24047164