Skip to content

Commit

Permalink
improve type hints
Browse files Browse the repository at this point in the history
  • Loading branch information
jrester committed Feb 14, 2022
1 parent f7498e6 commit ca05051
Show file tree
Hide file tree
Showing 14 changed files with 125 additions and 134 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.3.16]

- add `py.typed` file
- remove all the version pinning and drop support for powerwall version < 0.47.0
- add more type hints
- fix 'login_time' attribute in `LoginResponse`

## [0.3.15]

Expand Down
38 changes: 7 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
![Licence](https://img.shields.io/github/license/jrester/tesla_powerwall?style=for-the-badge)
![PyPI - Downloads](https://img.shields.io/pypi/dm/tesla_powerwall?color=blue&style=for-the-badge)
![PyPI](https://img.shields.io/pypi/v/tesla_powerwall?style=for-the-badge)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tesla_powerwall?style=for-the-badge)

Python Tesla Powerwall API for consuming a local endpoint. The API is by no means complete and mainly features methods which are considered to be of common use. If you feel like methods should be included you are welcome to open an Issue or create a Pull Request.

> Note: This is not an official API provided by Tesla and as such might fail at any time.
> Note: This is not an official API provided by Tesla and not affilated in anyways with Tesla.
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).
Powerwall Software versions from 1.47.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.


# Table of Contents <!-- omit in TOC -->
Expand All @@ -19,7 +16,6 @@ Powerwall Software versions from 1.45.0 to 1.50.1 as well as 20.40 to 21.39.1 ar
- [Setup](#setup)
- [Authentication](#authentication)
- [General](#general)
- [API versioning](#api-versioning)
- [Errors](#errors)
- [Response](#response)
- [Battery level](#battery-level)
Expand Down Expand Up @@ -62,13 +58,11 @@ powerwall = Powerwall(
endpoint="<ip of your powerwall>",
# Configure timeout; default is 10
timeout=10,
# Provide a requests.Session
# Provide a requests.Session or None to have one created
http_sesion=None,
# Whether to verify the SSL certificate or not
verify_ssl=False,
disable_insecure_warning=True,
# Set the API to expect a specific version of the powerwall software
pin_version=None
disable_insecure_warning=True
)
#=> <Powerwall ...>
```
Expand All @@ -79,7 +73,7 @@ powerwall = Powerwall(
### Authentication

Since version 20.49.0 authentication is required for all methods. For that reason you must call `login` before making a request to the API.
When you perform a request without being loggedin a `AccessDeniedError` will probably be thrown if the endpoint requires authentication.
When you perform a request without being loggedin a `AccessDeniedError` will be thrown.

To login you can either use `login` or `login_as`. `login` logs you in as `User.CUSTOMER` whereas with `login_as` you can choose a different user:

Expand Down Expand Up @@ -129,26 +123,6 @@ api.get_system_status_soe()

The `Powerwall` objet provides a wrapper around the API and exposes common methods.

#### API versioning

The powerwall API is inconsistent across different versions. This is why some versions may return different responses. If no version is specified the newest version is assumed.

If you are sure which version your powerwall has you can pin the Powerwall object to a version:

```python
from tesla_powerwall import Version
# Pin powerwall object
powerwall = Powerwall("<powerwall-ip>", pin_version="1.50.1")

# You can also pin a version after the powerwall object was created
powerwall.pin_version("20.40.3")
```

Otherwise you can let the API try to detect the version and pin it. This method should be prefered over the manual detection and pinning of the version:
```python
powerwall.detect_and_pin_version()
```

#### Errors

As the powerwall REST API varies widley between version and country it may happen that an attribute may not be included in your response. If that is the case a `MissingAttributeError` will be thrown indicating what attribute wasn't available.
Expand All @@ -170,6 +144,8 @@ assert_attribute(status.response, "version")
status.assert_attribute("version)
```

For retriving the version you could also alternativly use `powerwall.get_version`.

### Battery level

Get charge in percent:
Expand Down
Empty file added py.typed
Empty file.
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
name="tesla_powerwall",
author="Jrester",
author_email="[email protected]",
version='0.3.15',
version='0.3.16',
description="API for Tesla Powerwall",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/jrester/tesla_powerwall",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
],
install_requires=["requests>=2.22.0", "packaging>=20.5"],
package_data={
'tesla_powerwall': ["py.typed"]
},
install_requires=["requests>=2.22.0"],
)
8 changes: 4 additions & 4 deletions tesla_powerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
from .error import (
AccessDeniedError,
APIError,
MeterNotAvailableError,
MissingAttributeError,
PowerwallError,
PowerwallUnreachableError,
MeterNotAvailableError,
)
from .helpers import assert_attribute, convert_to_kw
from .powerwall import Powerwall
from .responses import (
Battery,
LoginResponse,
Meter,
MetersAggregates,
PowerwallStatus,
SiteInfo,
SiteMaster,
Solar,
Battery,
)
from .powerwall import Powerwall

VERSION = "0.3.15"
VERSION = "0.3.16"
10 changes: 5 additions & 5 deletions tesla_powerwall/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from http.client import responses
from json.decoder import JSONDecodeError
from typing import List
from urllib.parse import urljoin

import requests
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
from http.client import responses

from .error import AccessDeniedError, APIError, PowerwallUnreachableError

Expand Down Expand Up @@ -161,13 +161,13 @@ def login(
},
)

def logout(self):
def logout(self) -> None:
if not self.is_authenticated():
raise APIError("Must be logged in to log out")
# The api unsets the auth cookie and the token is invalidated
self.get("logout")

def close(self):
def close(self) -> None:
# Close the HTTP Session
# THis method is required for testing, so python doesn't complain about unclosed resources
self._http_session.close()
Expand Down Expand Up @@ -195,13 +195,13 @@ def get_sitemaster(self) -> dict:
def get_status(self) -> dict:
return self.get("status")

def get_customer_registration(self):
def get_customer_registration(self) -> dict:
return self.get("customer/registration")

def get_powerwalls(self):
return self.get("powerwalls")

def get_operation(self):
def get_operation(self) -> dict:
return self.get("operation")

def get_networks(self) -> list:
Expand Down
40 changes: 25 additions & 15 deletions tesla_powerwall/error.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
from typing import List, Union

from .const import MeterType


class PowerwallError(Exception):
def __init__(self, msg):
def __init__(self, msg: str):
super().__init__(msg)


class APIError(PowerwallError):
def __init__(self, error):
def __init__(self, error: str):
super().__init__("Powerwall api error: {}".format(error))


class MissingAttributeError(APIError):
def __init__(self, response: dict, attribute: str, url: str = None):
self.response = response
self.attribute = attribute
self.url = url
def __init__(self, response: dict, attribute: str, url: Union[str, None] = None):
self.response: dict = response
self.attribute: str = attribute
self.url: Union[str, None] = url

if url is None:
super().__init__(
Expand All @@ -29,19 +34,24 @@ def __init__(self, response: dict, attribute: str, url: str = None):


class PowerwallUnreachableError(PowerwallError):
def __init__(self, reason=None):
def __init__(self, reason: Union[str, None] = None):
msg = "Powerwall is unreachable"
self.reason = reason
self.reason: Union[str, None] = reason
if reason is not None:
msg = "{}: {}".format(msg, reason)
super().__init__(msg)


class AccessDeniedError(PowerwallError):
def __init__(self, resource, error=None, message=None):
self.resource = resource
self.error = error
self.message = message
def __init__(
self,
resource: str,
error: Union[str, None] = None,
message: Union[str, None] = None,
):
self.resource: str = resource
self.error: Union[str, None] = error
self.message: Union[str, None] = message
msg = "Access denied for resource {}".format(resource)
if error is not None:
if message is not None:
Expand All @@ -52,9 +62,9 @@ def __init__(self, resource, error=None, message=None):


class MeterNotAvailableError(PowerwallError):
def __init__(self, meter, available_meters):
self.meter = meter
self.available_meters = available_meters
def __init__(self, meter: MeterType, available_meters: List[MeterType]):
self.mete: MeterType = meter
self.available_meters: List[MeterType] = available_meters
super().__init__(
"Meter {} is not available at your powerwall. Following meters are available: {} ".format(
meter.value, available_meters
Expand Down
4 changes: 3 additions & 1 deletion tesla_powerwall/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

from .error import MissingAttributeError


Expand All @@ -10,7 +12,7 @@ def convert_to_kw(value: float, precision: int = 1) -> float:
return round(value / 1000, precision)


def assert_attribute(response: dict, attr: str, url: str = None):
def assert_attribute(response: dict, attr: str, url: Union[str, None] = None):
value = response.get(attr)
if value is None:
raise MissingAttributeError(response, attr, url)
Expand Down
29 changes: 16 additions & 13 deletions tesla_powerwall/powerwall.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Union, List
from typing import List, Union

import requests

from .api import API
Expand All @@ -15,27 +16,27 @@
SyncType,
User,
)
from .helpers import assert_attribute
from .responses import (
Battery,
LoginResponse,
Meter,
MetersAggregates,
PowerwallStatus,
SiteMaster,
SiteInfo,
SiteMaster,
Solar,
Battery,
)
from .helpers import assert_attribute


class Powerwall:
def __init__(
self,
endpoint: str,
timeout: int = 10,
http_session: requests.Session = None,
http_session: Union[requests.Session, None] = None,
verify_ssl: bool = False,
disable_insecure_warning: bool = True
disable_insecure_warning: bool = True,
):
self._api = API(
endpoint,
Expand Down Expand Up @@ -64,16 +65,16 @@ def login_as(
def login(self, password: str, email: str = "", force_sm_off: bool = False) -> dict:
return self.login_as(User.CUSTOMER, password, email, force_sm_off)

def logout(self):
def logout(self) -> None:
self._api.logout()

def is_authenticated(self) -> bool:
return self._api.is_authenticated()

def run(self):
def run(self) -> None:
self._api.get_sitemaster_run()

def stop(self):
def stop(self) -> None:
self._api.get_sitemaster_stop()

def get_charge(self) -> float:
Expand Down Expand Up @@ -120,7 +121,7 @@ def get_site_info(self) -> SiteInfo:
"""Returns information about the powerwall site"""
return SiteInfo(self._api.get_site_info())

def set_site_name(self, site_name: str):
def set_site_name(self, site_name: str) -> str:
return self._api.post_site_info_site_name({"site_name": site_name})

def get_status(self) -> PowerwallStatus:
Expand Down Expand Up @@ -162,10 +163,12 @@ def get_vin(self) -> str:

def get_version(self) -> str:
version_str = assert_attribute(self._api.get_status(), "version", "status")
return version_str.split(' ')[0] # newer versions include a sha trailer '21.44.1 c58c2df3'
return version_str.split(" ")[
0
] # newer versions include a sha trailer '21.44.1 c58c2df3'

def get_api(self):
def get_api(self) -> API:
return self._api

def close(self):
def close(self) -> None:
self._api.close()
Loading

0 comments on commit ca05051

Please sign in to comment.