diff --git a/CHANGELOG b/CHANGELOG index f23bdcd..0d7b8af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ # Changelog +## [0.3.14] + +- revert changes from 0.3.11: + - meters can now be accessed using the old, direct method (e.g. `meters.solar.instant_power`) + - if a meter is not available a `MeterNotAvailableError` will be thrown +- move from `distutils.version` to `packaging.version` + ## [0.3.13] Implement `system_status` endpoint (https://github.com/jrester/tesla_powerwall/issues/31): diff --git a/README.md b/README.md index dc43fad..e9a7ff2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Python Tesla Powerwall API for consuming a local endpoint. The API is by no mean > Note: This is not an official API provided by Tesla and as such might fail at any time. -Powerwall Software versions from 1.45.0 to 1.50.1 as well as 20.40 to 21.35.0 are tested, but others will probably work too. If you encounter an error regarding a change in the API of the Powerwall because your Powerwall has a different version than listed here please open an Issue to report this change so it can be fixed. +Powerwall Software versions from 1.45.0 to 1.50.1 as well as 20.40 to 21.39.1 are tested, but others will probably work too. If you encounter an error regarding a change in the API of the Powerwall because your Powerwall has a different version than listed here please open an Issue to report this change so it can be fixed. > For more information about versioning see [API versioning](#api-versioning). @@ -272,15 +272,20 @@ from tesla_powerwall import MeterType meters = powerwall.get_meters() #=> +# access meter, but may return None when meter is not available meters.get_meter(MeterType.SOLAR) #=> -``` +# access meter, but may raise MeterNotAvailableError when the meter is not available at your powerwall (e.g. no solar panels installed) +meters.solar +#=> -Available meters are: `solar`, `site`, `load` and `battery`. If you have a generator you can also access it with the `generator` MeterType. +# get all available meters at the current powerwall +meters.meters +#=> [, , , ] +``` -> Note: if the powerwall you are working with has no solar panels installed `get_meter(MeterType.SOLAR)` returns `None` -> With the attribute `MetersAggregates.meters` you can get the available meters in the response +Available meters are: `solar`, `site`, `load`, `battery` and `generator`. Some of those meters might not be available based on the installation and raise MeterNotAvailableError when accessed. #### Current power supply/draw @@ -306,14 +311,6 @@ meters.battery.is_active(precision=5) > Note: For MeterType.LOAD `is_drawing_from` **always** returns `False` because it cannot be drawn from `load`. -`Meter.get_power` is just a convenience method which is equivalent to: - -```python -from tesla_powerwall.helpers import convert_to_kw - -convert_to_kw(meters.solar.instant_power, precision=1) -``` - #### Energy exported/imported Get energy exported/imported in watts with `energy_exported` and `energy_imported`. For the values in kWh use `get_energy_exported` and `get_energy_imported`: diff --git a/setup.py b/setup.py index 1fdf600..4a14be4 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ name="tesla_powerwall", author="Jrester", author_email="jrester379@gmail.com", - version='0.3.13', + version='0.3.14', description="API for Tesla Powerwall", long_description=long_description, long_description_content_type="text/markdown", @@ -17,5 +17,5 @@ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", ], - install_requires=["requests>=2.22.0"], + install_requires=["requests>=2.22.0", "packaging>=20.5"], ) diff --git a/tesla_powerwall/__init__.py b/tesla_powerwall/__init__.py index 6043a2d..14cc176 100644 --- a/tesla_powerwall/__init__.py +++ b/tesla_powerwall/__init__.py @@ -18,6 +18,7 @@ MissingAttributeError, PowerwallError, PowerwallUnreachableError, + MeterNotAvailableError, ) from .helpers import assert_attribute, convert_to_kw from .responses import ( @@ -32,4 +33,4 @@ ) from .powerwall import Powerwall -VERSION = "0.3.13" +VERSION = "0.3.14" diff --git a/tesla_powerwall/error.py b/tesla_powerwall/error.py index af1d8aa..4a1b007 100644 --- a/tesla_powerwall/error.py +++ b/tesla_powerwall/error.py @@ -49,3 +49,14 @@ def __init__(self, resource, error=None, message=None): else: msg = "{}: {}".format(msg, error) super().__init__(msg) + + +class MeterNotAvailableError(PowerwallError): + def __init__(self, meter, available_meters): + self.meter = meter + self.available_meters = available_meters + super().__init__( + "Meter {} is not available at your powerwall. Following meters are available: {} ".format( + meter.value, available_meters + ) + ) diff --git a/tesla_powerwall/powerwall.py b/tesla_powerwall/powerwall.py index 806ddce..f2681ce 100644 --- a/tesla_powerwall/powerwall.py +++ b/tesla_powerwall/powerwall.py @@ -1,6 +1,6 @@ from typing import Union, List import requests -from distutils import version +from packaging import version from .api import API from .const import ( @@ -87,7 +87,9 @@ def get_charge(self) -> float: return assert_attribute(self._api.get_system_status_soe(), "percentage", "soe") def get_energy(self) -> int: - return assert_attribute(self._api.get_system_status(), "nominal_energy_remaining", "system_status") + return assert_attribute( + self._api.get_system_status(), "nominal_energy_remaining", "system_status" + ) def get_sitemaster(self) -> SiteMaster: return SiteMaster(self._api.get_sitemaster()) @@ -104,10 +106,14 @@ def get_grid_status(self) -> GridStatus: return GridStatus(status) def get_capacity(self) -> float: - return assert_attribute(self._api.get_system_status(), "nominal_full_pack_energy", "system_status") + return assert_attribute( + self._api.get_system_status(), "nominal_full_pack_energy", "system_status" + ) def get_batteries(self) -> List[Battery]: - batteries = assert_attribute(self._api.get_system_status(), "battery_blocks", "system_status") + batteries = assert_attribute( + self._api.get_system_status(), "battery_blocks", "system_status" + ) return [Battery(battery) for battery in batteries] def is_grid_services_active(self) -> bool: @@ -177,7 +183,7 @@ def pin_version(self, vers: Union[str, version.Version]): if isinstance(vers, version.Version): self._pin_version = vers else: - self._pin_version = version.LooseVersion(vers) + self._pin_version = version.Version(vers) def get_pinned_version(self) -> version.Version: return self._pin_version diff --git a/tesla_powerwall/responses.py b/tesla_powerwall/responses.py index 3169032..06000f9 100644 --- a/tesla_powerwall/responses.py +++ b/tesla_powerwall/responses.py @@ -8,6 +8,7 @@ Roles, ) from .helpers import assert_attribute, convert_to_kw +from .error import MeterNotAvailableError class Response: @@ -106,6 +107,16 @@ def __init__(self, response): super().__init__(response) self.meters = [MeterType(key) for key in response.keys()] + def __getattribute__(self, attr): + if attr.upper() in MeterType.__dict__: + m = MeterType(attr) + if m in self.meters: + return self.get_meter(m) + else: + raise MeterNotAvailableError(m, self.meters) + else: + return object.__getattribute__(self, attr) + def get_meter(self, meter: MeterType) -> Meter: if meter in self.meters: return Meter(meter, self.assert_attribute(meter.value)) @@ -276,8 +287,8 @@ def model(self): def power_rating_watts(self): return self.assert_attribute("power_rating_watts") -class Battery(Response): +class Battery(Response): @property def part_number(self): return self.assert_attribute("PackagePartNumber") @@ -310,7 +321,7 @@ def energy_remaining(self) -> int: Returns: int: energy in watts - """ + """ return self.assert_attribute("nominal_energy_remaining") @property @@ -329,4 +340,4 @@ def wobble_detected(self) -> bool: Returns: bool: detected """ - return self.assert_attribute("wobble_detected") \ No newline at end of file + return self.assert_attribute("wobble_detected") diff --git a/tests/integration/test_powerwall.py b/tests/integration/test_powerwall.py index a8c899a..86a3761 100644 --- a/tests/integration/test_powerwall.py +++ b/tests/integration/test_powerwall.py @@ -14,6 +14,7 @@ from tests.integration import POWERWALL_IP, POWERWALL_PASSWORD + class TestPowerwall(unittest.TestCase): def setUp(self) -> None: self.powerwall = Powerwall(POWERWALL_IP) diff --git a/tests/unit/test_powerwall.py b/tests/unit/test_powerwall.py index 5d0c5ff..0e36a84 100644 --- a/tests/unit/test_powerwall.py +++ b/tests/unit/test_powerwall.py @@ -1,23 +1,17 @@ -import json -import os import unittest import datetime -from distutils import version -import requests +from packaging import version import responses from responses import GET, POST, Response, add from tesla_powerwall import ( API, - AccessDeniedError, - APIError, Meter, MetersAggregates, Powerwall, - PowerwallUnreachableError, + MeterNotAvailableError, SiteMaster, - SiteInfo, GridStatus, DeviceType, assert_attribute, @@ -50,10 +44,10 @@ def test_get_api(self): def test_pins_version_on_creation(self): pw = Powerwall(ENDPOINT, pin_version="1.49.0") - self.assertEqual(pw.get_pinned_version(), version.LooseVersion("1.49.0")) + self.assertEqual(pw.get_pinned_version(), version.Version("1.49.0")) - pw = Powerwall(ENDPOINT, pin_version=version.LooseVersion("1.49.0")) - self.assertEqual(pw.get_pinned_version(), version.LooseVersion("1.49.0")) + pw = Powerwall(ENDPOINT, pin_version=version.Version("1.49.0")) + self.assertEqual(pw.get_pinned_version(), version.Version("1.49.0")) @responses.activate def test_get_charge(self): @@ -95,17 +89,10 @@ def test_get_meters(self): meters.meters, [MeterType.SITE, MeterType.BATTERY, MeterType.LOAD, MeterType.SOLAR], ) + self.assertIsInstance(meters.load, Meter) self.assertIsInstance(meters.get_meter(MeterType.LOAD), Meter) - - @responses.activate - def test_meter(self): - add( - Response( - responses.GET, - url=f"{ENDPOINT}meters/aggregates", - json=METERS_AGGREGATES_RESPONSE, - ) - ) + with self.assertRaises(MeterNotAvailableError): + meters.generator @responses.activate def test_is_sending(self): @@ -203,14 +190,18 @@ def test_get_backup_reserved_percentage(self): add( Response(responses.GET, url=f"{ENDPOINT}operation", json=OPERATION_RESPONSE) ) - self.assertEqual(self.powerwall.get_backup_reserve_percentage(), 5.000019999999999) + self.assertEqual( + self.powerwall.get_backup_reserve_percentage(), 5.000019999999999 + ) @responses.activate def test_get_operation_mode(self): add( Response(responses.GET, url=f"{ENDPOINT}operation", json=OPERATION_RESPONSE) ) - self.assertEqual(self.powerwall.get_operation_mode(), OperationMode.SELF_CONSUMPTION) + self.assertEqual( + self.powerwall.get_operation_mode(), OperationMode.SELF_CONSUMPTION + ) @responses.activate def test_get_version(self): @@ -220,14 +211,20 @@ def test_get_version(self): @responses.activate def test_detect_and_pin_version(self): add(Response(responses.GET, url=f"{ENDPOINT}status", json=STATUS_RESPONSE)) - vers = version.LooseVersion("1.50.1") + vers = version.Version("1.50.1") pw = Powerwall(ENDPOINT) self.assertEqual(pw.detect_and_pin_version(), vers) self.assertEqual(pw._pin_version, vers) @responses.activate def test_system_status(self): - add(Response(responses.GET, url=f"{ENDPOINT}system_status", json=SYSTEM_STATUS_RESPONSE)) + add( + Response( + responses.GET, + url=f"{ENDPOINT}system_status", + json=SYSTEM_STATUS_RESPONSE, + ) + ) self.assertEqual(self.powerwall.get_capacity(), 28078) self.assertEqual(self.powerwall.get_energy(), 14807) diff --git a/tox.ini b/tox.ini index aba97ce..14e1256 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,16 @@ [tox] -envlist = py39 +envlist = testenv isolated_build = True [testenv] setenv = PYTHONPATH = {toxinidir} -deps = unittest2 - responses -commands = unit2 discover {posargs:tests/unit} +deps = responses + packaging +commands = python -m unittest discover {posargs:tests/unit} [testenv:unit] -commands = unit2 discover tests/unit +commands = python -m unittest discover tests/unit [testenv:integration] passenv = POWERWALL_IP POWERWALL_PASSWORD -commands = unit2 discover tests/integration \ No newline at end of file +commands = python -m unittest discover tests/integration \ No newline at end of file