Repository: jrester/tesla_powerwall (language: python)

Issue:
Feature Request: Per Phase Meter Data
Hi,

Just wondering if it would be possible to add additional detail to the meter statistics?

The values I am particularly interested in are the per-phase power and voltage values for the site and solar meters from the https://X.X.X.X/api/meters/site and https://X.X.X.X/api/meters/solar APIs respectively:

- real_power_a
- real_power_b
- real_power_c
- v_l1n
- v_l2n
- v_l3n
  
Sample from site meter:

>	{
		"id": 0,
		"location": "site",
		"type": "synchrometerX",
		"cts": [
			true,
			true,
			true,
			false
		],
		"inverted": [
			false,
			false,
			false,
			false
		],
		"connection": {
			"short_id": "XXXX",
			"device_serial": "YYYYY",
			"https_conf": {}
		},
		"Cached_readings": {
			"last_communication_time": "2023-07-17T16:27:36.390511647+10:00",
			"instant_power": -22,
			"instant_reactive_power": -1154,
			"instant_apparent_power": 1154.2096863222037,
			"frequency": 0,
			"energy_exported": 2502052.53320083,
			"energy_imported": 2970955.180779687,
			"instant_average_voltage": 409.3644348662122,
			"instant_average_current": 5.529,
			"i_a_current": 3.02,
			"i_b_current": 1.311,
			"i_c_current": 1.198,
			"last_phase_voltage_communication_time": "2023-07-17T16:27:36.390568769+10:00",
			"v_l1n": 235.82,
			"v_l2n": 234.34,
			"v_l3n": 238.88,
			"last_phase_power_communication_time": "2023-07-17T16:27:36.390511647+10:00",
			"real_power_a": -144,
			"real_power_b": 48,
			"real_power_c": 74,
			"reactive_power_a": -583,
			"reactive_power_b": -297,
			"reactive_power_c": -274,
			"last_phase_energy_communication_time": "0001-01-01T00:00:00Z",
			"serial_number": "JBL19236C4D232",
			"timeout": 60000000000,
			"instant_total_current": 5.529
		}
	}
]

Happy to test/assist if I can.

Feature Request: Per Phase Meter Data
Hi,

Just wondering if it would be possible to add additional detail to the meter statistics?

The values I am particularly interested in are the per-phase power and voltage values for the site and solar meters from the https://X.X.X.X/api/meters/site and https://X.X.X.X/api/meters/solar APIs respectively:

- real_power_a
- real_power_b
- real_power_c
- v_l1n
- v_l2n
- v_l3n
  
Sample from site meter:

>	{
		"id": 0,
		"location": "site",
		"type": "synchrometerX",
		"cts": [
			true,
			true,
			true,
			false
		],
		"inverted": [
			false,
			false,
			false,
			false
		],
		"connection": {
			"short_id": "XXXX",
			"device_serial": "YYYYY",
			"https_conf": {}
		},
		"Cached_readings": {
			"last_communication_time": "2023-07-17T16:27:36.390511647+10:00",
			"instant_power": -22,
			"instant_reactive_power": -1154,
			"instant_apparent_power": 1154.2096863222037,
			"frequency": 0,
			"energy_exported": 2502052.53320083,
			"energy_imported": 2970955.180779687,
			"instant_average_voltage": 409.3644348662122,
			"instant_average_current": 5.529,
			"i_a_current": 3.02,
			"i_b_current": 1.311,
			"i_c_current": 1.198,
			"last_phase_voltage_communication_time": "2023-07-17T16:27:36.390568769+10:00",
			"v_l1n": 235.82,
			"v_l2n": 234.34,
			"v_l3n": 238.88,
			"last_phase_power_communication_time": "2023-07-17T16:27:36.390511647+10:00",
			"real_power_a": -144,
			"real_power_b": 48,
			"real_power_c": 74,
			"reactive_power_a": -583,
			"reactive_power_b": -297,
			"reactive_power_c": -274,
			"last_phase_energy_communication_time": "0001-01-01T00:00:00Z",
			"serial_number": "JBL19236C4D232",
			"timeout": 60000000000,
			"instant_total_current": 5.529
		}
	}
]

Happy to test/assist if I can.



Discussion / hints:
Hey @dh99999, I think the endpoints would be a great addition! I will take a look at the API endpoints and let you know, as soon as it is implemented.
That would be great - thanks!

Happy to help test!
Hey @dh99999, I think the endpoints would be a great addition! I will take a look at the API endpoints and let you know, as soon as it is implemented.
That would be great - thanks!

Happy to help test!

Fix patch:
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..175de89
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,1 @@
+layout python
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index c29e1c5..fb4a130 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -6,7 +6,7 @@ on:
       - created
       - published
       - released
-  
+
 jobs:
   deploy:
 
diff --git a/.gitignore b/.gitignore
index 75f19d1..5a330a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -123,4 +123,5 @@ dmypy.json
 .pyre/
 
 .js
-.vscode
\ No newline at end of file
+.vscode
+.direnv
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..fdeed13
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,29 @@
+repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    # Ruff version.
+    rev: v0.0.285
+    hooks:
+      - id: ruff
+        args: [--fix, --exit-non-zero-on-fix]
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v1.5.1
+    hooks:
+      - id: mypy
+        additional_dependencies: ["types-requests"]
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.4.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: pretty-format-json
+        args: [--autofix]
+  - repo: https://github.com/pycqa/isort
+    rev: 5.12.0
+    hooks:
+      - id: isort
+        args: [--settings-path=pyproject.toml]
+  - repo: https://github.com/psf/black
+    rev: 23.7.0
+    hooks:
+      - id: black
+        args: [--config=pyproject.toml]
diff --git a/CHANGELOG b/CHANGELOG
index ca6bbf6..1ef14a1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,14 @@
 # Changelog
 
+## [0.4.0]
+
+- fix logout (https://github.com/jrester/tesla_powerwall/issues/50)
+- add meter details for site and solar (https://github.com/jrester/tesla_powerwall/issues/48)
+- rework response handling to now parse the responses directly instead of relying on lazy evaluation
+- extend pre-commit hooks
+- move to pyproject.toml and remove old setup.py
+
+
 ## [0.3.19]
 
 - add ability to take powerwall on/off grid. Thanks to @daniel-simpson (https://github.com/jrester/tesla_powerwall/pull/42)
@@ -28,7 +37,7 @@
 
 ## [0.3.14]
 
-- revert changes from 0.3.11: 
+- 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`
@@ -45,4 +54,4 @@ Implement `system_status` endpoint (https://github.com/jrester/tesla_powerwall/i
 
 ## [0.3.11]
 
-- meters of `MetersAggregates` can now only be accessed via `get_meter` (https://github.com/home-assistant/core/issues/56660)
\ No newline at end of file
+- meters of `MetersAggregates` can now only be accessed via `get_meter` (https://github.com/home-assistant/core/issues/56660)
diff --git a/LICENSE b/LICENSE
index 76ed278..53f8e84 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2022 Jrester
+Copyright (c) 2023 Jrester
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/README.md b/README.md
index 0ee085f..03c3fd6 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,16 @@
 ![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)
 
-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 not affilated in anyways with Tesla.
-
-Powerwall Software versions from 1.47.0 to 1.50.1 as well as 20.40 to 22.9.2 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.
+Python Tesla Powerwall API for consuming a local endpoint.
+> Note: This is not an official API provided by Tesla and this project is not affilated with Tesla in any way.
 
+Powerwall Software versions from 1.47.0 to 1.50.1 as well as 20.40 to 22.9.2 are tested, but others will probably work too.
 
 # Table of Contents <!-- omit in TOC -->
 
 - [Installation](#installation)
+- [Limitations](#limitations)
+    - [Adjusting Backup Reserve Percentage](#adjusting-backup-reserve-percentage)
 - [Usage](#usage)
     - [Setup](#setup)
     - [Authentication](#authentication)
@@ -28,6 +28,7 @@ Powerwall Software versions from 1.47.0 to 1.50.1 as well as 20.40 to 22.9.2 are
         - [Aggregates](#aggregates)
         - [Current power supply/draw](#current-power-supplydraw)
         - [Energy exported/imported](#energy-exportedimported)
+        - [Details](#details)
     - [Device Type](#device-type)
     - [Grid Status](#grid-status)
     - [Operation mode](#operation-mode)
@@ -35,6 +36,7 @@ Powerwall Software versions from 1.47.0 to 1.50.1 as well as 20.40 to 22.9.2 are
     - [Gateway DIN](#gateway-din)
     - [VIN](#vin)
     - [Off-grid status](#off-grid-status-set-island-mode)
+
 ## Installation
 
 Install the library via pip:
@@ -43,6 +45,13 @@ Install the library via pip:
 $ pip install tesla_powerwall
 ```
 
+## Limitations
+
+### Adjusting Backup Reserve Percentage
+
+Currently it is not possible to control the Backup Percentage, because you need to be logged in as installer, which requires physical switch toggle. There is an ongoing discussion about a possible solution [here](https://github.com/vloschiavo/powerwall2/issues/55).
+However, if you believe there exists a solution, feel free to open an issue detailing the solution.
+
 ## Usage
 
 For a basic Overview of the functionality of this library you can take a look at `examples/example.py`:
@@ -67,7 +76,7 @@ powerwall = Powerwall(
     endpoint="<ip of your powerwall>",
     # Configure timeout; default is 10
     timeout=10,
-    # Provide a requests.Session or None to have one created
+    # Provide a requests.Session or None. If None is provided, a Session will be created.
     http_session=None,
     # Whether to verify the SSL certificate or not
     verify_ssl=False,
@@ -82,7 +91,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 be thrown.
+When you perform a request without being authenticated, an `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:
 
@@ -110,6 +119,8 @@ powerwall.is_authenticated()
 
 # Logout
 powerwall.logout()
+powerwall.is_authenticated()
+#=> False
 ```
 
 ### General
@@ -132,29 +143,6 @@ api.get_system_status_soe()
 
 The `Powerwall` objet provides a wrapper around the API and exposes common methods.
 
-#### 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. 
-
-#### Response
-
-Responses are usally wrapped inside a `Response` object to provide convenience methods. An Example is the `Meter` class which is a sublass of `Response`. Each `Response` object includes the `response` member which consists of the plain json response. 
-
-```python
-from helpers import assert_attribute
-
-status = powerwall.get_status()
-#=> <PowerwallStatus ...>
-
-status.version
-# is the same as
-assert_attribute(status.response, "version")
-# or
-status.assert_attribute("version")
-```
-
-For retriving the version you could also alternativly use `powerwall.get_version`.
-
 ### Battery level
 
 Get charge in percent:
@@ -221,9 +209,9 @@ status.device_type
 ### Sitemaster
 
 ```python
-sm = powerwall.sitemaster 
+sm = powerwall.sitemaster
 #=> <SiteMaster ...>
-sm.status 
+sm.status
 #=> StatusUp
 sm.running
 #=> true
@@ -264,10 +252,10 @@ 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
-#=> <Meter ...>
+#=> <MeterResponse ...>
 
 # get all available meters at the current powerwall
-meters.meters
+meters.meters.keys()
 #=> [<MeterType.SITE: 'site'>, <MeterType.BATTERY: 'battery'>, <MeterType.LOAD: 'load'>, <MeterType.SOLAR: 'solar'>]
 ```
 
@@ -312,6 +300,30 @@ meters.battery.get_energy_imported()
 #=> 7576.6 (kWh)
 ```
 
+### Details
+
+You can receive more detailed information about the meters `site` and `solar`:
+
+```python
+meter_details = powerwall.get_meter_site() # or get_meter_solar() for the solar meter
+#=> <MeterDetailsResponse ...>
+readings = meter_details.readings
+#=> <MeterDetailsReadings ...>
+readings.real_power_a # same for real_power_b and real_power_c
+#=> 619.13532458
+readings.i_a_current # same for i_b_current and i_c_current
+#=> 3.02
+readings.v_l1n # smae for v_l2n and v_l3n
+#=> 235.82
+readings.instant_power
+#=> -18.000023458
+readings.is_sending()
+```
+
+As `MeterDetailsReadings` inherits from `MeterResponse` (which is used in `MetersAggratesResponse`) it exposes the same data and methods.
+
+> For the meters battery and grid no additional details are provided, therefore no methods exist for those meters
+
 ### Device Type
 
 ```python
@@ -321,7 +333,7 @@ powerwall.get_device_type()
 
 ### Grid Status
 
-Get current grid status. 
+Get current grid status.
 
 ```python
 powerwall.get_grid_status()
@@ -361,7 +373,7 @@ vin = powerwall.get_vin()
 
 ### Off-grid status (Set Island mode)
 
-Take your powerwall on- and off-grid similar to the "Take off-grid" button in the Tesla app. 
+Take your powerwall on- and off-grid similar to the "Take off-grid" button in the Tesla app.
 
 #### Set powerwall to off-grid (Islanded)
 
@@ -374,3 +386,27 @@ powerwall.set_island_mode(IslandMode.OFFGRID)
 ```python
 powerwall.set_island_mode(IslandMode.ONGRID)
 ```
+
+# Development
+
+## Building
+
+```sh
+$ python -m build
+```
+
+## Testing
+
+### Unit-Tests
+
+To run unit tests use tox:
+
+```sh
+$ tox -e unit
+```
+
+### Integration-Tests
+
+```sh
+$ tox -e integration
+```
diff --git a/examples/example.py b/examples/example.py
index 4ba8099..cb4034b 100644
--- a/examples/example.py
+++ b/examples/example.py
@@ -1,5 +1,6 @@
 import os
-from tesla_powerwall import Powerwall, Meter
+
+from tesla_powerwall import MeterResponse, Powerwall
 
 
 def getenv(var):
@@ -9,7 +10,7 @@ def getenv(var):
     return val
 
 
-def print_meter_row(meter_data: Meter):
+def print_meter_row(meter_data: MeterResponse):
     print(
         "{:>8} {:>8} {:>17} {:>17} {!r:>8} {!r:>17} {!r:>17}".format(
             meter_data.meter.value,
@@ -42,7 +43,7 @@ def print_meter_row(meter_data: Meter):
     ("Grid Status", power_wall.get_grid_status().value),
     ("Backup Reserve (%)", round(power_wall.get_backup_reserve_percentage())),
     ("Device Type", power_wall.get_device_type().value),
-    ("Software Version", power_wall.get_version())
+    ("Software Version", power_wall.get_version()),
 ]
 
 
@@ -62,5 +63,6 @@ def print_meter_row(meter_data: Meter):
         "Sending to",
     )
 )
-for meter in meters_agg.meters:
-    print_meter_row(meters_agg.get_meter(meter))
+
+for meter in meters_agg.meters.values():
+    print_meter_row(meter)
diff --git a/examples/influxDB.py b/examples/influxDB.py
deleted file mode 100644
index 6650f07..0000000
--- a/examples/influxDB.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# This is an Example of how to use the tesla_powerwall API and the influxdb Client to generate and store
-# Monitoring Data in a Time Series Database. InfluxDB is natively compatible with https://grafana.com
-
-#Imports
-import os
-from time import sleep
-import influxdb_client
-from influxdb_client.client.write_api import SYNCHRONOUS
-from tesla_powerwall import Powerwall
-
-
-# Variables
-## InfluxDB
-bucket = "<my-bucket>"
-org = "<my-org>"
-token = "<my-token>"
-url="http://localhost:8086"
-
-client = influxdb_client.InfluxDBClient(
-    url=url,
-    token=token,
-    org=org
-)
-
-write_api = client.write_api(write_options=SYNCHRONOUS)
-
-## Powerwall
-
-ip = os.getenv("POWERWALL_IP")
-if ip is None:
-    raise ValueError("POWERWALL_IP must be set")
-
-email = os.getenv("POWERWALL_EMAIL")
-password = os.getenv("POWERWALL_PASSWORD")
-
-power_wall = Powerwall(ip)
-
-# Program
-print("Current charge: {}".format(power_wall.get_charge()))
-print("Device Type: {}".format(power_wall.get_device_type()))
-
-## Sending Data
-
-while True:
-    p = influxdb_client.Point("Measurement").field("Charge", power_wall.get_charge)
-    write_api.write(bucket=bucket, org=org, record = p)
-    sleep(1)
diff --git a/pyproject.toml b/pyproject.toml
index 9dd4cc8..32158ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,40 @@
 [build-system]
-requires = ["requests>=2.22.0","setuptools >= 35.0.2"]
-build-backend = "setuptools.build_meta"
\ No newline at end of file
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "tesla_powerwall"
+version = "0.4.0"
+description = "A simple API for accessing the Tesla Powerwall over your local network"
+readme = "README.md"
+license = { file = "LICENSE"}
+classifiers = [
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3",
+]
+keywords = ["api", "tesla", "powerwall", "tesla_powerwall"]
+dependencies = [
+  "requests>=2.22.0"
+]
+
+[project.urls]
+Homepage = "https://github.com/jrester/tesla_powerwall"
+
+[project.optional-dependencies]
+test = [
+ "tox",
+ "pre-commit",
+]
+
+[tool.ruff]
+ignore-init-module-imports = true
+
+[tool.coverage.run]
+source = ["tesla_powerwall"]
+
+[tool.isort]
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+ensure_newline_before_comments = true
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 855fc6e..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[coverage:run]
-omit = */.local/*
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0cf9d2c..0000000
--- a/setup.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from setuptools import setup, find_packages
-
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setup(
-    name="tesla_powerwall",
-    author="Jrester",
-    author_email="jrester379@gmail.com",
-    version='0.3.19',
-    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(exclude=["tests*"]),
-    classifiers=[
-        "Programming Language :: Python :: 3",
-        "License :: OSI Approved :: MIT License"
-    ],
-    package_data={
-        'tesla_powerwall': ["py.typed"]
-    },
-    install_requires=["requests>=2.22.0"],
-)
diff --git a/tesla_powerwall/__init__.py b/tesla_powerwall/__init__.py
index 1c52c2d..fcc5f30 100644
--- a/tesla_powerwall/__init__.py
+++ b/tesla_powerwall/__init__.py
@@ -1,6 +1,7 @@
+# ruff: noqa: F401
+
 from .api import API
 from .const import (
-    DEFAULT_KW_ROUND_PERSICION,
     SUPPORTED_OPERATION_MODES,
     DeviceType,
     GridState,
@@ -15,7 +16,7 @@
 )
 from .error import (
     AccessDeniedError,
-    APIError,
+    ApiError,
     MeterNotAvailableError,
     MissingAttributeError,
     PowerwallError,
@@ -24,14 +25,18 @@
 from .helpers import assert_attribute, convert_to_kw
 from .powerwall import Powerwall
 from .responses import (
-    Battery,
+    BatteryResponse,
     LoginResponse,
-    Meter,
-    MetersAggregates,
-    PowerwallStatus,
-    SiteInfo,
-    SiteMaster,
-    Solar,
+    MeterDetailsReadings,
+    MeterDetailsResponse,
+    MeterResponse,
+    MetersAggregatesResponse,
+    PowerwallStatusResponse,
+    SiteInfoResponse,
+    SiteMasterResponse,
+    SolarResponse,
 )
 
-VERSION = "0.3.19"
+VERSION = "0.4.0"
+
+__all__ = list(filter(lambda n: not n.startswith("_"), globals().keys()))
diff --git a/tesla_powerwall/api.py b/tesla_powerwall/api.py
index f69aa77..4fc8827 100644
--- a/tesla_powerwall/api.py
+++ b/tesla_powerwall/api.py
@@ -1,13 +1,13 @@
 from http.client import responses
 from json.decoder import JSONDecodeError
-from typing import List
+from typing import Any, List, Optional
 from urllib.parse import urljoin
 
 import requests
 from urllib3 import disable_warnings
 from urllib3.exceptions import InsecureRequestWarning
 
-from .error import AccessDeniedError, APIError, PowerwallUnreachableError
+from .error import AccessDeniedError, ApiError, PowerwallUnreachableError
 
 
 class API(object):
@@ -15,11 +15,10 @@ def __init__(
         self,
         endpoint: str,
         timeout: int = 10,
-        http_session: requests.Session = None,
+        http_session: Optional[requests.Session] = None,
         verify_ssl: bool = False,
         disable_insecure_warning: bool = True,
     ) -> None:
-
         if disable_insecure_warning:
             disable_warnings(InsecureRequestWarning)
 
@@ -50,7 +49,7 @@ def _parse_endpoint(endpoint: str) -> str:
     @staticmethod
     def _handle_error(response: requests.Response) -> None:
         if response.status_code == 404:
-            raise APIError(
+            raise ApiError(
                 "The url {} returned error 404".format(response.request.path_url)
             )
 
@@ -68,7 +67,7 @@ def _handle_error(response: requests.Response) -> None:
                 )
 
         if response.text is not None and len(response.text) > 0:
-            raise APIError(
+            raise ApiError(
                 "API returned status code '{}: {}' with body: {}".format(
                     response.status_code,
                     responses.get(response.status_code),
@@ -76,7 +75,7 @@ def _handle_error(response: requests.Response) -> None:
                 )
             )
         else:
-            raise APIError(
+            raise ApiError(
                 "API returned status code '{}: {}' ".format(
                     response.status_code, responses.get(response.status_code)
                 )
@@ -93,7 +92,7 @@ def _process_response(self, response: requests.Response) -> dict:
         try:
             response_json = response.json()
         except JSONDecodeError:
-            raise APIError(
+            raise ApiError(
                 "Error while decoding json of response: {}".format(response.text)
             )
 
@@ -103,14 +102,14 @@ def _process_response(self, response: requests.Response) -> dict:
         # Newer versions of the powerwall do not return such values anymore
         # Kept for backwards compability or if the API changes again
         if "error" in response_json:
-            raise APIError(response_json["error"])
+            raise ApiError(response_json["error"])
 
         return response_json
 
     def url(self, path: str):
         return urljoin(self._endpoint, path)
 
-    def get(self, path: str, headers: dict = {}) -> dict:
+    def get(self, path: str, headers: dict = {}) -> Any:
         try:
             response = self._http_session.get(
                 url=self.url(path),
@@ -121,7 +120,7 @@ def get(self, path: str, headers: dict = {}) -> dict:
             requests.exceptions.ConnectionError,
             requests.exceptions.ReadTimeout,
         ) as e:
-            raise PowerwallUnreachableError(e)
+            raise PowerwallUnreachableError(str(e))
 
         return self._process_response(response)
 
@@ -130,7 +129,7 @@ def post(
         path: str,
         payload: dict,
         headers: dict = {},
-    ) -> dict:
+    ) -> Any:
         try:
             response = self._http_session.post(
                 url=self.url(path),
@@ -142,7 +141,7 @@ def post(
             requests.exceptions.ConnectionError,
             requests.exceptions.ReadTimeout,
         ) as e:
-            raise PowerwallUnreachableError(e)
+            raise PowerwallUnreachableError(str(e))
 
         return self._process_response(response)
 
@@ -150,9 +149,12 @@ def is_authenticated(self) -> bool:
         return "AuthCookie" in self._http_session.cookies.keys()
 
     def login(
-        self, username: str, email: str, password: str, force_sm_off: bool = False
+        self,
+        username: str,
+        email: str,
+        password: str,
+        force_sm_off: bool = False,
     ) -> dict:
-
         # force_sm_off is referred to as 'shouldForceLogin' in the web source code
         return self.post(
             "login/Basic",
@@ -166,13 +168,14 @@ def login(
 
     def logout(self) -> None:
         if not self.is_authenticated():
-            raise APIError("Must be logged in to log out")
+            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) -> None:
         # Close the HTTP Session
-        # THis method is required for testing, so python doesn't complain about unclosed resources
+        # THis method is required for testing,
+        # so python doesn't complain about unclosed resources
         self._http_session.close()
 
     # Endpoints are mapped to one method by <verb>_<path> so they can be easily accessed
diff --git a/tesla_powerwall/const.py b/tesla_powerwall/const.py
index b395b80..0d91383 100644
--- a/tesla_powerwall/const.py
+++ b/tesla_powerwall/const.py
@@ -25,10 +25,12 @@ class GridStatus(Enum):
     TRANSITION_TO_GRID = "SystemTransitionToGrid"  # Used in version 1.46.0
     TRANSITION_TO_ISLAND = "SystemTransitionToIsland"
 
+
 class IslandMode(Enum):
     OFFGRID = "intentional_reconnect_failsafe"
     ONGRID = "backup"
 
+
 class GridState(Enum):
     COMPLIANT = "Grid_Compliant"
     QUALIFINY = "Grid_Qualifying"
diff --git a/tesla_powerwall/error.py b/tesla_powerwall/error.py
index 95d73d6..42aef92 100644
--- a/tesla_powerwall/error.py
+++ b/tesla_powerwall/error.py
@@ -8,12 +8,12 @@ def __init__(self, msg: str):
         super().__init__(msg)
 
 
-class APIError(PowerwallError):
+class ApiError(PowerwallError):
     def __init__(self, error: str):
         super().__init__("Powerwall api error: {}".format(error))
 
 
-class MissingAttributeError(APIError):
+class MissingAttributeError(ApiError):
     def __init__(self, response: dict, attribute: str, url: Union[str, None] = None):
         self.response: dict = response
         self.attribute: str = attribute
@@ -27,7 +27,8 @@ def __init__(self, response: dict, attribute: str, url: Union[str, None] = None)
             )
         else:
             super().__init__(
-                "The attribute '{}' is expected in the response for '{}' but is missing.".format(
+                "The attribute '{}' is expected in the response for \
+                 '{}' but is missing.".format(
                     attribute, url
                 )
             )
@@ -66,7 +67,8 @@ def __init__(self, meter: MeterType, available_meters: List[MeterType]):
         self.meter: MeterType = meter
         self.available_meters: List[MeterType] = available_meters
         super().__init__(
-            "Meter {} is not available at your powerwall. Following meters are available: {} ".format(
+            "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 eb1f67a..6173888 100644
--- a/tesla_powerwall/powerwall.py
+++ b/tesla_powerwall/powerwall.py
@@ -3,30 +3,18 @@
 import requests
 
 from .api import API
-from .const import (
-    DEFAULT_KW_ROUND_PERSICION,
-    SUPPORTED_OPERATION_MODES,
-    DeviceType,
-    GridState,
-    GridStatus,
-    IslandMode,
-    LineStatus,
-    MeterType,
-    OperationMode,
-    Roles,
-    SyncType,
-    User,
-)
+from .const import DeviceType, GridStatus, IslandMode, OperationMode, User
+from .error import ApiError
 from .helpers import assert_attribute
 from .responses import (
-    Battery,
+    BatteryResponse,
     LoginResponse,
-    Meter,
-    MetersAggregates,
-    PowerwallStatus,
-    SiteInfo,
-    SiteMaster,
-    Solar,
+    MeterDetailsResponse,
+    MetersAggregatesResponse,
+    PowerwallStatusResponse,
+    SiteInfoResponse,
+    SiteMasterResponse,
+    SolarResponse,
 )
 
 
@@ -53,7 +41,7 @@ def login_as(
         password: str,
         email: str,
         force_sm_off: bool = False,
-    ) -> dict:
+    ) -> LoginResponse:
         if isinstance(user, User):
             user = user.value
 
@@ -61,9 +49,11 @@ def login_as(
         # The api returns an auth cookie which is automatically set
         # so there is no need to further process the response
 
-        return LoginResponse(response)
+        return LoginResponse.from_dict(response)
 
-    def login(self, password: str, email: str = "", force_sm_off: bool = False) -> dict:
+    def login(
+        self, password: str, email: str = "", force_sm_off: bool = False
+    ) -> LoginResponse:
         return self.login_as(User.CUSTOMER, password, email, force_sm_off)
 
     def logout(self) -> None:
@@ -83,33 +73,53 @@ def get_charge(self) -> Union[float, int]:
 
     def get_energy(self) -> int:
         return assert_attribute(
-            self._api.get_system_status(), "nominal_energy_remaining", "system_status"
+            self._api.get_system_status(),
+            "nominal_energy_remaining",
+            "system_status",
         )
 
-    def get_sitemaster(self) -> SiteMaster:
-        return SiteMaster(self._api.get_sitemaster())
+    def get_sitemaster(self) -> SiteMasterResponse:
+        return SiteMasterResponse.from_dict(self._api.get_sitemaster())
+
+    def get_meters(self) -> MetersAggregatesResponse:
+        return MetersAggregatesResponse.from_dict(self._api.get_meters_aggregates())
+
+    def get_meter_site(self) -> MeterDetailsResponse:
+        meter_response = self._api.get_meters_site()
+        if meter_response is None or len(meter_response) == 0:
+            raise ApiError("The powerwall returned no values for the site meter")
+
+        return MeterDetailsResponse.from_dict(meter_response[0])
+
+    def get_meter_solar(self) -> MeterDetailsResponse:
+        meter_response = self._api.get_meters_solar()
+        if meter_response is None or len(meter_response) == 0:
+            raise ApiError("The powerwall returned no values for the solar meter")
 
-    def get_meters(self) -> MetersAggregates:
-        return MetersAggregates(self._api.get_meters_aggregates())
+        return MeterDetailsResponse.from_dict(meter_response[0])
 
     def get_grid_status(self) -> GridStatus:
         """Returns the current grid status."""
         status = assert_attribute(
-            self._api.get_system_status_grid_status(), "grid_status", "grid_status"
+            self._api.get_system_status_grid_status(),
+            "grid_status",
+            "grid_status",
         )
 
         return GridStatus(status)
 
     def get_capacity(self) -> float:
         return assert_attribute(
-            self._api.get_system_status(), "nominal_full_pack_energy", "system_status"
+            self._api.get_system_status(),
+            "nominal_full_pack_energy",
+            "system_status",
         )
 
-    def get_batteries(self) -> List[Battery]:
+    def get_batteries(self) -> List[BatteryResponse]:
         batteries = assert_attribute(
             self._api.get_system_status(), "battery_blocks", "system_status"
         )
-        return [Battery(battery) for battery in batteries]
+        return [BatteryResponse.from_dict(battery) for battery in batteries]
 
     def is_grid_services_active(self) -> bool:
         return assert_attribute(
@@ -118,15 +128,15 @@ def is_grid_services_active(self) -> bool:
             "grid_status",
         )
 
-    def get_site_info(self) -> SiteInfo:
+    def get_site_info(self) -> SiteInfoResponse:
         """Returns information about the powerwall site"""
-        return SiteInfo(self._api.get_site_info())
+        return SiteInfoResponse.from_dict(self._api.get_site_info())
 
-    def set_site_name(self, site_name: str) -> str:
+    def set_site_name(self, site_name: str) -> dict:
         return self._api.post_site_info_site_name({"site_name": site_name})
 
-    def get_status(self) -> PowerwallStatus:
-        return PowerwallStatus(self._api.get_status())
+    def get_status(self) -> PowerwallStatusResponse:
+        return PowerwallStatusResponse.from_dict(self._api.get_status())
 
     def get_device_type(self) -> DeviceType:
         """Returns the device type of the powerwall"""
@@ -156,14 +166,19 @@ def get_backup_reserve_percentage(self) -> float:
             self._api.get_operation(), "backup_reserve_percent", "operation"
         )
 
-    def get_solars(self) -> List[Solar]:
-        return [Solar(solar) for solar in self._api.get_solars()]
+    def get_solars(self) -> List[SolarResponse]:
+        return [SolarResponse.from_dict(solar) for solar in self._api.get_solars()]
 
     def get_vin(self) -> str:
         return assert_attribute(self._api.get_config(), "vin", "config")
 
     def set_island_mode(self, mode: IslandMode) -> IslandMode:
-        return IslandMode(assert_attribute(self._api.post_islanding_mode({"island_mode": mode.value}), "island_mode"))
+        return IslandMode(
+            assert_attribute(
+                self._api.post_islanding_mode({"island_mode": mode.value}),
+                "island_mode",
+            )
+        )
 
     def get_version(self) -> str:
         version_str = assert_attribute(self._api.get_status(), "version", "status")
diff --git a/tesla_powerwall/responses.py b/tesla_powerwall/responses.py
index 16f9d57..1616308 100644
--- a/tesla_powerwall/responses.py
+++ b/tesla_powerwall/responses.py
@@ -1,82 +1,55 @@
 import re
+from dataclasses import dataclass
 from datetime import datetime, timedelta
-from typing import List
+from typing import Any, Dict, List, Optional
 
 from .const import DEFAULT_KW_ROUND_PERSICION, DeviceType, MeterType, Roles
 from .error import MeterNotAvailableError
-from .helpers import assert_attribute, convert_to_kw
+from .helpers import convert_to_kw
 
 
-class Response:
-    def __init__(self, response: dict) -> None:
-        self.response = response
-
-    def assert_attribute(self, attr: str) -> any:
-        return assert_attribute(self.response, attr)
+@dataclass
+class ResponseBase:
+    _raw: dict
 
     def __repr__(self) -> str:
-        return str(self.response)
-
-
-class Meter(Response):
-    """
-    Attributes:
-    - last_communication_time
-    - instant_power
-    - instant_reactive_power
-    - instant_apparent_power
-    - frequency
-    - energy_exported
-    - energy_imported
-    - instant_average_voltage
-    - instant_total_current
-    - i_a_current
-    - i_b_current
-    - i_c_current
-    - timeout
-    """
-
-    def __init__(self, meter: MeterType, response) -> None:
-        self.meter = meter
-        super().__init__(response)
-
-    @property
-    def instant_power(self) -> float:
-        return self.assert_attribute("instant_power")
-
-    @property
-    def last_communication_time(self) -> str:
-        return self.assert_attribute("last_communication_time")
-
-    @property
-    def frequency(self) -> float:
-        return self.assert_attribute("frequency")
-
-    @property
-    def energy_exported(self) -> float:
-        return self.assert_attribute("energy_exported")
+        return str(self._raw)
+
+
+@dataclass
+class MeterResponse(ResponseBase):
+    meter: MeterType
+    instant_power: float
+    last_communication_time: str
+    frequency: float
+    energy_exported: float
+    energy_imported: float
+    instant_total_current: float
+    instant_average_voltage: float
+
+    @staticmethod
+    def from_dict(meter: MeterType, src: dict) -> "MeterResponse":
+        return MeterResponse(
+            src,
+            meter=meter,
+            instant_power=src["instant_power"],
+            last_communication_time=src["last_communication_time"],
+            frequency=src["frequency"],
+            energy_exported=src["energy_exported"],
+            energy_imported=src["energy_imported"],
+            instant_total_current=src["instant_total_current"],
+            instant_average_voltage=src["instant_average_voltage"],
+        )
 
     def get_energy_exported(self, precision=DEFAULT_KW_ROUND_PERSICION) -> float:
         return convert_to_kw(self.energy_exported, precision)
 
-    @property
-    def energy_imported(self) -> float:
-        return self.assert_attribute("energy_imported")
-
     def get_energy_imported(self, precision=DEFAULT_KW_ROUND_PERSICION) -> float:
         return convert_to_kw(self.energy_imported, precision)
 
-    @property
-    def instant_total_current(self) -> float:
-        return self.assert_attribute("instant_total_current")
-
     def get_instant_total_current(self, precision=DEFAULT_KW_ROUND_PERSICION) -> float:
         return round(self.instant_total_current, precision)
 
-    @property
-    def average_voltage(self) -> float:
-        return self.assert_attribute("instant_average_voltage")
-
     def get_power(self, precision=DEFAULT_KW_ROUND_PERSICION) -> float:
         return convert_to_kw(self.instant_power, precision)
 
@@ -98,242 +71,218 @@ def is_sending_to(self, precision=DEFAULT_KW_ROUND_PERSICION) -> bool:
             return self.get_power(precision) < 0
 
 
-class MetersAggregates(Response):
-    def __init__(self, response) -> str:
-        super().__init__(response)
-        self.meters = [MeterType(key) for key in response.keys()]
-
-    def __getattribute__(self, attr) -> any:
+@dataclass
+class MeterDetailsReadings(MeterResponse):
+    real_power_a: Optional[float]
+    real_power_b: Optional[float]
+    real_power_c: Optional[float]
+
+    i_a_current: Optional[float]
+    i_b_current: Optional[float]
+    i_c_current: Optional[float]
+
+    v_l1n: Optional[float]
+    v_l2n: Optional[float]
+    v_l3n: Optional[float]
+
+    @staticmethod
+    def from_dict(meter: MeterType, src: dict) -> "MeterDetailsReadings":
+        meter_response = MeterResponse.from_dict(meter, src)
+        return MeterDetailsReadings(
+            real_power_a=src.get("real_power_a"),
+            real_power_b=src.get("real_power_b"),
+            real_power_c=src.get("real_power_c"),
+            i_a_current=src.get("i_a_current"),
+            i_b_current=src.get("i_b_current"),
+            i_c_current=src.get("i_c_current"),
+            v_l1n=src.get("v_l1n"),
+            v_l2n=src.get("v_l2n"),
+            v_l3n=src.get("v_l3n"),
+            # Populate with the values from the base class
+            **meter_response.__dict__
+        )
+
+
+@dataclass
+class MeterDetailsResponse(ResponseBase):
+    location: MeterType
+    readings: MeterDetailsReadings
+
+    @staticmethod
+    def from_dict(src: dict) -> "MeterDetailsResponse":
+        location = MeterType(src["location"])
+        readings = MeterDetailsReadings.from_dict(location, src["Cached_readings"])
+        return MeterDetailsResponse(src, location=location, readings=readings)
+
+
+class MetersAggregatesResponse(ResponseBase):
+    @staticmethod
+    def from_dict(src: dict) -> "MetersAggregatesResponse":
+        meters = {
+            MeterType(key): MeterResponse.from_dict(MeterType(key), value)
+            for key, value in src.items()
+        }
+        return MetersAggregatesResponse(src, meters)
+
+    def __init__(self, response: dict, meters: Dict[MeterType, MeterResponse]) -> None:
+        self._raw = response
+        self.meters = meters
+
+    def __getattribute__(self, attr) -> Any:
         if attr.upper() in MeterType.__dict__:
             m = MeterType(attr)
             if m in self.meters:
-                return self.get_meter(m)
+                return self.meters[m]
             else:
-                raise MeterNotAvailableError(m, self.meters)
+                raise MeterNotAvailableError(m, list(self.meters.keys()))
         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))
-        else:
-            return None
-
-
-class SiteMaster(Response):
-    """
-    Attributes:
-    - running
-    - connected_to_tesla
-    - status
-    - power_supply_mode
-    """
-
-    def __init__(self, response) -> None:
-        super().__init__(response)
-
-    @property
-    def status(self) -> str:
-        return self.assert_attribute("status")
-
-    @property
-    def is_running(self) -> bool:
-        return self.assert_attribute("running")
-
-    @property
-    def is_connected_to_tesla(self) -> bool:
-        return self.assert_attribute("connected_to_tesla")
-
-    @property
-    def is_power_supply_mode(self) -> bool:
-        return self.assert_attribute("power_supply_mode")
-
-
-class SiteInfo(Response):
-    """
-    Attributes:
-    - max_site_meter_power_kW
-    - min_site_meter_power_kW
-    - nominal_system_energy_kWh
-    - nominal_system_power_kW
-    - max_system_energy_kWh
-    - max_system_power_kW
-    - site_name
-    - timezone
-    - grid_code
-    """
-
-    def __init__(self, response) -> None:
-        super().__init__(response)
-
-    @property
-    def nominal_system_energy(self) -> int:
-        return self.assert_attribute("nominal_system_energy_kWh")
-
-    @property
-    def site_name(self) -> str:
-        return self.assert_attribute("site_name")
-
-    @property
-    def timezone(self) -> str:
-        return self.assert_attribute("timezone")
-
-
-class PowerwallStatus(Response):
-    """
-    Attributes:
-    * start_time
-    * up_time_seconds
-    * is_new
-    * version
-    * device_type
-    * commission_count
-    * sync_type
-    * git_hash
-    """
+    def get_meter(self, meter: MeterType) -> Optional[MeterResponse]:
+        return self.meters.get(meter)
+
+
+@dataclass
+class SiteMasterResponse(ResponseBase):
+    status: str
+    is_running: bool
+    is_connected_to_tesla: bool
+    is_power_supply_mode: bool
+
+    @staticmethod
+    def from_dict(src: dict) -> "SiteMasterResponse":
+        return SiteMasterResponse(
+            src,
+            status=src["status"],
+            is_running=src["running"],
+            is_connected_to_tesla=src["connected_to_tesla"],
+            is_power_supply_mode=src["power_supply_mode"],
+        )
+
+
+@dataclass
+class SiteInfoResponse(ResponseBase):
+    nominal_system_energy: int
+    nominal_system_power: int
+    site_name: str
+    timezone: str
+
+    @staticmethod
+    def from_dict(src: dict) -> "SiteInfoResponse":
+        return SiteInfoResponse(
+            src,
+            nominal_system_energy=src["nominal_system_energy_kWh"],
+            nominal_system_power=src["nominal_system_power_kW"],
+            site_name=src["site_name"],
+            timezone=src["timezone"],
+        )
+
+
+@dataclass
+class PowerwallStatusResponse(ResponseBase):
+    start_time: datetime
+    up_time_seconds: timedelta
+    version: str
+    device_type: DeviceType
+    commission_count: int
+    sync_type: str
+    git_hash: str
 
     _START_TIME_FORMAT = "%Y-%m-%d %H:%M:%S %z"
     _UP_TIME_SECONDS_REGEX = re.compile(
         r"^((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$"
     )
 
-    def _parse_uptime_seconds(self, up_time_seconds: str) -> timedelta:
-        match = PowerwallStatus._UP_TIME_SECONDS_REGEX.match(up_time_seconds)
+    @staticmethod
+    def _parse_uptime_seconds(up_time_seconds: str) -> timedelta:
+        match = PowerwallStatusResponse._UP_TIME_SECONDS_REGEX.match(up_time_seconds)
         if not match:
             raise ValueError(
                 "Unable to parse up time seconds {}".format(up_time_seconds)
             )
 
         time_params = {}
-        for (name, param) in match.groupdict().items():
+        for name, param in match.groupdict().items():
             if param:
                 time_params[name] = float(param)
 
         return timedelta(**time_params)
 
-    @property
-    def up_time_seconds(self) -> timedelta:
-        up_time_seconds = assert_attribute(self.response, "up_time_seconds")
-        return self._parse_uptime_seconds(up_time_seconds)
-
-    @property
-    def start_time(self) -> datetime:
-        start_time = assert_attribute(self.response, "start_time")
-        return datetime.strptime(start_time, self._START_TIME_FORMAT)
-
-    @property
-    def version(self) -> str:
-        return self.assert_attribute("version")
-
-    @property
-    def device_type(self) -> DeviceType:
-        return DeviceType(self.assert_attribute("device_type"))
-
-
-class LoginResponse(Response):
-    """
-    Attributes
-    - email
-    - firstname
-    - lastname
-    - roles
-    - token
-    - provider
-    - loginTime
-    """
-
-    @property
-    def firstname(self) -> str:
-        return self.assert_attribute("firstname")
-
-    @property
-    def lastname(self) -> str:
-        return self.assert_attribute("lastname")
-
-    @property
-    def token(self) -> str:
-        return self.assert_attribute("token")
-
-    @property
-    def roles(self) -> List[Roles]:
-        return [Roles(role) for role in self.assert_attribute("roles")]
-
-    @property
-    def login_time(self):
-        return self.assert_attribute("loginTime")
-
-
-class Solar(Response):
-    """
-    Attributes
-    - brand
-    - model
-    - power_rating_watts
-    """
-
-    @property
-    def brand(self) -> str:
-        return self.assert_attribute("brand")
-
-    @property
-    def model(self) -> str:
-        return self.assert_attribute("model")
-
-    @property
-    def power_rating_watts(self) -> int:
-        return self.assert_attribute("power_rating_watts")
-
-
-class Battery(Response):
-    @property
-    def part_number(self) -> str:
-        return self.assert_attribute("PackagePartNumber")
-
-    @property
-    def serial_number(self) -> str:
-        return self.assert_attribute("PackageSerialNumber")
-
-    @property
-    def energy_charged(self) -> int:
-        """get the amount of energy that was ever charged
-
-        Returns:
-            int: energy in watts
-        """
-        return self.assert_attribute("energy_charged")
-
-    @property
-    def energy_discharged(self) -> int:
-        """get the amount of energy that was ever discharged
-
-        Returns:
-            int: energy in watts
-        """
-        return self.assert_attribute("energy_discharged")
-
-    @property
-    def energy_remaining(self) -> int:
-        """get the remaining charged energy
-
-        Returns:
-            int: energy in watts
-        """
-        return self.assert_attribute("nominal_energy_remaining")
-
-    @property
-    def capacity(self) -> int:
-        """get the capacity of a battery
-
-        Returns:
-            int: energy in watts
-        """
-        return self.assert_attribute("nominal_full_pack_energy")
-
-    @property
-    def wobble_detected(self) -> bool:
-        """get whether a wobble was detected
-
-        Returns:
-            bool: detected
-        """
-        return self.assert_attribute("wobble_detected")
+    @staticmethod
+    def from_dict(src: dict) -> "PowerwallStatusResponse":
+        start_time = datetime.strptime(
+            src["start_time"], PowerwallStatusResponse._START_TIME_FORMAT
+        )
+        up_time_seconds = PowerwallStatusResponse._parse_uptime_seconds(
+            src["up_time_seconds"]
+        )
+        return PowerwallStatusResponse(
+            src,
+            start_time=start_time,
+            up_time_seconds=up_time_seconds,
+            version=src["version"],
+            device_type=DeviceType(src["device_type"]),
+            commission_count=src["commission_count"],
+            sync_type=src["sync_type"],
+            git_hash=src["git_hash"],
+        )
+
+
+@dataclass
+class LoginResponse(ResponseBase):
+    firstname: str
+    lastname: str
+    token: str
+    roles: List[Roles]
+    login_time: str
+
+    @staticmethod
+    def from_dict(src: dict) -> "LoginResponse":
+        return LoginResponse(
+            src,
+            firstname=src["firstname"],
+            lastname=src["lastname"],
+            token=src["token"],
+            roles=[Roles(role) for role in src["roles"]],
+            login_time=src["loginTime"],
+        )
+
+
+@dataclass
+class SolarResponse(ResponseBase):
+    brand: str
+    model: str
+    power_rating_watts: int
+
+    @staticmethod
+    def from_dict(src: dict) -> "SolarResponse":
+        return SolarResponse(
+            src,
+            brand=src["brand"],
+            model=src["model"],
+            power_rating_watts=src["power_rating_watts"],
+        )
+
+
+@dataclass
+class BatteryResponse(ResponseBase):
+    part_number: str
+    serial_number: str
+    energy_charged: int
+    energy_discharged: int
+    energy_remaining: int
+    capacity: int
+    wobble_detected: bool
+
+    @staticmethod
+    def from_dict(src: dict) -> "BatteryResponse":
+        return BatteryResponse(
+            src,
+            part_number=src["PackagePartNumber"],
+            serial_number=src["PackageSerialNumber"],
+            energy_charged=src["energy_charged"],
+            energy_discharged=src["energy_discharged"],
+            energy_remaining=src["nominal_energy_remaining"],
+            capacity=src["nominal_full_pack_energy"],
+            wobble_detected=src["wobble_detected"],
+        )
diff --git a/tox.ini b/tox.ini
index 14e1256..f1ddd94 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,8 @@
 [tox]
 envlist = testenv
-isolated_build = True
 
 [testenv]
-setenv = PYTHONPATH = {toxinidir}
-deps = responses 
-        packaging
+deps = responses
 commands = python -m unittest discover {posargs:tests/unit}
 
 [testenv:unit]
@@ -13,4 +10,4 @@ commands = python -m unittest discover tests/unit
 
 [testenv:integration]
 passenv = POWERWALL_IP POWERWALL_PASSWORD
-commands = python -m unittest discover tests/integration
\ No newline at end of file
+commands = python -m unittest discover tests/integration


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/test_powerwall.py b/tests/integration/test_powerwall.py
index a5b3a89..2fe46e3 100644
--- a/tests/integration/test_powerwall.py
+++ b/tests/integration/test_powerwall.py
@@ -1,17 +1,14 @@
 import unittest
 from time import sleep
 
-from tesla_powerwall import (
-    GridStatus,
-    IslandMode,
-    Meter,
-    MetersAggregates,
-    MeterType,
-    Powerwall,
-    SiteInfo,
-    SiteMaster,
+from tesla_powerwall import GridStatus, IslandMode, MeterType, Powerwall
+from tesla_powerwall.responses import (
+    MeterResponse,
+    MetersAggregatesResponse,
+    PowerwallStatusResponse,
+    SiteInfoResponse,
+    SiteMasterResponse,
 )
-from tesla_powerwall.responses import PowerwallStatus
 from tests.integration import POWERWALL_IP, POWERWALL_PASSWORD
 
 
@@ -25,25 +22,26 @@ def tearDown(self) -> None:
 
     def test_get_charge(self) -> None:
         charge = self.powerwall.get_charge()
-        if(charge < 100):
+        if charge < 100:
             self.assertIsInstance(charge, float)
         else:
             self.assertEqual(charge, 100)
 
     def test_get_meters(self) -> None:
         meters = self.powerwall.get_meters()
-        self.assertIsInstance(meters, MetersAggregates)
+        self.assertIsInstance(meters, MetersAggregatesResponse)
 
-        self.assertIsInstance(meters.get_meter(MeterType.BATTERY), Meter)
+        self.assertIsInstance(meters.get_meter(MeterType.BATTERY), MeterResponse)
 
         for meter_type in meters.meters:
             meter = meters.get_meter(meter_type)
+            assert meter is not None
             meter.energy_exported
             meter.energy_imported
             meter.instant_power
             meter.last_communication_time
             meter.frequency
-            meter.average_voltage
+            meter.instant_average_voltage
             meter.get_energy_exported()
             meter.get_energy_imported()
             self.assertIsInstance(meter.get_power(), float)
@@ -54,7 +52,7 @@ def test_get_meters(self) -> None:
     def test_sitemaster(self) -> None:
         sitemaster = self.powerwall.get_sitemaster()
 
-        self.assertIsInstance(sitemaster, SiteMaster)
+        self.assertIsInstance(sitemaster, SiteMasterResponse)
 
         sitemaster.status
         sitemaster.is_running
@@ -64,7 +62,7 @@ def test_sitemaster(self) -> None:
     def test_site_info(self) -> None:
         site_info = self.powerwall.get_site_info()
 
-        self.assertIsInstance(site_info, SiteInfo)
+        self.assertIsInstance(site_info, SiteInfoResponse)
 
         site_info.nominal_system_energy
         site_info.site_name
@@ -94,7 +92,7 @@ def test_grid_status(self) -> None:
 
     def test_status(self) -> None:
         status = self.powerwall.get_status()
-        self.assertIsInstance(status, PowerwallStatus)
+        self.assertIsInstance(status, PowerwallStatusResponse)
         status.up_time_seconds
         status.start_time
         status.version
@@ -103,10 +101,10 @@ def test_islanding(self) -> None:
         initial_grid_status = self.powerwall.get_grid_status()
         self.assertIsInstance(initial_grid_status, GridStatus)
 
-        if(initial_grid_status == GridStatus.CONNECTED):
+        if initial_grid_status == GridStatus.CONNECTED:
             self.go_offline()
             self.go_online()
-        elif(initial_grid_status == GridStatus.ISLANDED):
+        elif initial_grid_status == GridStatus.ISLANDED:
             self.go_offline()
             self.go_online()
 
@@ -122,13 +120,18 @@ def go_online(self) -> None:
         self.wait_until_grid_status(GridStatus.CONNECTED)
         self.assertEqual(self.powerwall.get_grid_status(), GridStatus.CONNECTED)
 
-    def wait_until_grid_status(self, expectedStatus: GridStatus, sleepTime: int = 1, maxCycles: int = 20) -> None:
+    def wait_until_grid_status(
+        self,
+        expectedStatus: GridStatus,
+        sleepTime: int = 1,
+        maxCycles: int = 20,
+    ) -> None:
         cycles = 0
         observedStatus: GridStatus
 
         while cycles < maxCycles:
             observedStatus = self.powerwall.get_grid_status()
-            if(observedStatus == expectedStatus):
+            if observedStatus == expectedStatus:
                 break
             sleep(sleepTime)
             cycles = cycles + 1
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
index 1ccf1a0..f121edb 100644
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -1,18 +1,26 @@
 import json
-import os
+from pathlib import Path
 
 ENDPOINT = "https://1.1.1.1/api/"
 
-PREFIX = "tests/unit/fixtures"
+FIXTURE_BASE_PATH = Path("tests/unit/fixtures")
 
 
-def _load_fixtures():
-    for file in os.listdir("tests/unit/fixtures"):
-        if file.endswith(".json"):
-            file_path = os.path.join(PREFIX, file)
-            with open(file_path) as f:
-                name = file[:-5].upper() + "_RESPONSE"
-                globals()[name] = json.loads(f.read())
+def load_fixture(name: str):
+    path = FIXTURE_BASE_PATH / name
+    with open(path) as f:
+        return json.load(f)
 
 
-_load_fixtures()
+GRID_STATUS_RESPONSE = load_fixture("grid_status.json")
+ISLANDING_MODE_OFFGRID_RESPONSE = load_fixture("islanding_mode_offgrid.json")
+ISLANDING_MODE_ONGRID_RESPONSE = load_fixture("islanding_mode_ongrid.json")
+METER_SITE_RESPONSE = load_fixture("meter_site.json")
+METER_SOLAR_RESPONSE = load_fixture("meter_solar.json")
+METERS_AGGREGATES_RESPONSE = load_fixture("meters_aggregates.json")
+OPERATION_RESPONSE = load_fixture("operation.json")
+POWERWALLS_RESPONSE = load_fixture("powerwalls.json")
+SITE_INFO_RESPONSE = load_fixture("site_info.json")
+SITEMASTER_RESPONSE = load_fixture("sitemaster.json")
+STATUS_RESPONSE = load_fixture("status.json")
+SYSTEM_STATUS_RESPONSE = load_fixture("system_status.json")
diff --git a/tests/unit/fixtures/grid_status.json b/tests/unit/fixtures/grid_status.json
index 67eabc7..d6ed8f0 100644
--- a/tests/unit/fixtures/grid_status.json
+++ b/tests/unit/fixtures/grid_status.json
@@ -1,1 +1,4 @@
-{"grid_status": "SystemGridConnected", "grid_services_active": false}
\ No newline at end of file
+{
+  "grid_services_active": false,
+  "grid_status": "SystemGridConnected"
+}
diff --git a/tests/unit/fixtures/islanding_mode_offgrid.json b/tests/unit/fixtures/islanding_mode_offgrid.json
index 7a1a9cd..a2ad891 100644
--- a/tests/unit/fixtures/islanding_mode_offgrid.json
+++ b/tests/unit/fixtures/islanding_mode_offgrid.json
@@ -1,1 +1,3 @@
-{"island_mode": "intentional_reconnect_failsafe"}
\ No newline at end of file
+{
+  "island_mode": "intentional_reconnect_failsafe"
+}
diff --git a/tests/unit/fixtures/islanding_mode_ongrid.json b/tests/unit/fixtures/islanding_mode_ongrid.json
index 6db2c51..b83a928 100644
--- a/tests/unit/fixtures/islanding_mode_ongrid.json
+++ b/tests/unit/fixtures/islanding_mode_ongrid.json
@@ -1,1 +1,3 @@
-{"island_mode": "backup"}
\ No newline at end of file
+{
+  "island_mode": "backup"
+}
diff --git a/tests/unit/fixtures/meter_site.json b/tests/unit/fixtures/meter_site.json
new file mode 100644
index 0000000..98a583d
--- /dev/null
+++ b/tests/unit/fixtures/meter_site.json
@@ -0,0 +1,58 @@
+[
+  {
+    "Cached_readings": {
+      "energy_exported": 1152130.009160081,
+      "energy_exported_a": 1192873.1919444446,
+      "energy_exported_b": 1713.8994444444445,
+      "energy_imported": 15614764.694437858,
+      "energy_imported_a": 15654958.373333333,
+      "energy_imported_b": 6.539722222222222,
+      "frequency": 50,
+      "i_a_current": 0,
+      "i_b_current": 0,
+      "i_c_current": 0,
+      "instant_apparent_power": 431.96520416598025,
+      "instant_average_current": 0,
+      "instant_average_voltage": 123.87999878078699,
+      "instant_power": -18.00000076368451,
+      "instant_reactive_power": -431.5900109857321,
+      "instant_total_current": 0,
+      "last_communication_time": "2023-04-08T12:55:00.180984196+01:00",
+      "last_phase_energy_communication_time": "2023-04-07T14:44:17.625943287+01:00",
+      "last_phase_power_communication_time": "2023-04-08T12:55:00.180984196+01:00",
+      "last_phase_voltage_communication_time": "2023-04-08T12:55:00.181027196+01:00",
+      "reactive_power_a": -431.4800109863281,
+      "reactive_power_b": -0.10999999940395355,
+      "real_power_a": -17.950000762939453,
+      "real_power_b": -0.05000000074505806,
+      "serial_number": "OBB1234567890",
+      "timeout": 1500000000,
+      "v_l1n": 247.55999755859375,
+      "v_l2n": 0.20000000298023224,
+      "version": "1.7.1-Tesla"
+    },
+    "connection": {
+      "device_serial": "OBB1234567890",
+      "https_conf": {},
+      "ip_address": "PWRview-12345",
+      "neurio_connected": true,
+      "port": 443,
+      "short_id": "12345"
+    },
+    "cts": [
+      true,
+      true,
+      false,
+      false
+    ],
+    "id": 0,
+    "inverted": [
+      false,
+      false,
+      false,
+      false
+    ],
+    "location": "site",
+    "type": "neurio_tcp"
+  }
+]
diff --git a/tests/unit/fixtures/meter_solar.json b/tests/unit/fixtures/meter_solar.json
new file mode 100644
index 0000000..07a336e
--- /dev/null
+++ b/tests/unit/fixtures/meter_solar.json
@@ -0,0 +1,53 @@
+[
+  {
+    "Cached_readings": {
+      "energy_exported": 30088.217501777115,
+      "energy_exported_a": 30104.78111111111,
+      "energy_imported": 18346702.535001777,
+      "energy_imported_a": 18336727.701388888,
+      "frequency": 49.95000076293945,
+      "i_a_current": 0,
+      "i_b_current": 0,
+      "i_c_current": 0,
+      "instant_apparent_power": 2869.7488146485034,
+      "instant_average_current": 0,
+      "instant_average_voltage": 250.72000122070312,
+      "instant_power": 2867.159912109375,
+      "instant_reactive_power": -121.87000274658203,
+      "instant_total_current": 0,
+      "last_communication_time": "2023-04-08T13:01:26.68807076+01:00",
+      "last_phase_energy_communication_time": "2023-04-08T08:02:36.955494546+01:00",
+      "last_phase_power_communication_time": "2023-04-08T13:01:26.68807076+01:00",
+      "last_phase_voltage_communication_time": "2023-04-08T13:01:26.68811376+01:00",
+      "reactive_power_a": -121.87000274658203,
+      "real_power_a": 2867.159912109375,
+      "serial_number": "OBB1234567890",
+      "timeout": 1500000000,
+      "v_l1n": 250.72000122070312,
+      "version": "1.7.1-Tesla"
+    },
+    "connection": {
+      "device_serial": "OBB1234567890",
+      "https_conf": {},
+      "ip_address": "PWRview-12345",
+      "neurio_connected": true,
+      "port": 443,
+      "short_id": "12345"
+    },
+    "cts": [
+      false,
+      false,
+      false,
+      true
+    ],
+    "id": 0,
+    "inverted": [
+      false,
+      false,
+      false,
+      false
+    ],
+    "location": "solar",
+    "type": "neurio_tcp"
+  }
+]
diff --git a/tests/unit/fixtures/meters_aggregates.json b/tests/unit/fixtures/meters_aggregates.json
index 2dc0e44..6b6e096 100644
--- a/tests/unit/fixtures/meters_aggregates.json
+++ b/tests/unit/fixtures/meters_aggregates.json
@@ -1,1 +1,62 @@
-{"site": {"last_communication_time": "2020-04-09T05:50:38.989687241-07:00", "instant_power": -5347.455078125, "instant_reactive_power": -664.1942901611328, "instant_apparent_power": 5388.546173843879, "frequency": 49.99971389770508, "energy_exported": 5512641.122754764, "energy_imported": 9852397.795532543, "instant_average_voltage": 232.0439249674479, "instant_total_current": 0, "i_a_current": 0, "i_b_current": 0, "i_c_current": 0, "timeout": 1500000000}, "battery": {"last_communication_time": "2020-04-09T05:50:38.990165237-07:00", "instant_power": -10, "instant_reactive_power": 600, "instant_apparent_power": 600.0833275470999, "frequency": 49.995000000000005, "energy_exported": 4379890, "energy_imported": 5265110, "instant_average_voltage": 230.8, "instant_total_current": -0.4, "i_a_current": 0, "i_b_current": 0, "i_c_current": 0, "timeout": 1500000000}, "load": {"last_communication_time": "2020-04-09T05:50:38.974944676-07:00", "instant_power": 734.1549565813606, "instant_reactive_power": -469.988307011022, "instant_apparent_power": 871.7066645380579, "frequency": 49.99971389770508, "energy_exported": 0, "energy_imported": 24751111.13611111, "instant_average_voltage": 232.0439249674479, "instant_total_current": 3.1638620001982423, "i_a_current": 0, "i_b_current": 0, "i_c_current": 0, "timeout": 1500000000}, "solar": {"last_communication_time": "2020-04-09T05:50:38.974944676-07:00", "instant_power": 6099.032958984375, "instant_reactive_power": -422.27491760253906, "instant_apparent_power": 6113.633873631454, "frequency": 49.95012283325195, "energy_exported": 21296639.987777833, "energy_imported": 65.52444450131091, "instant_average_voltage": 232.1537322998047, "instant_total_current": 0, "i_a_current": 0, "i_b_current": 0, "i_c_current": 0, "timeout": 1500000000}}
\ No newline at end of file
+{
+  "battery": {
+    "energy_exported": 4379890,
+    "energy_imported": 5265110,
+    "frequency": 49.995000000000005,
+    "i_a_current": 0,
+    "i_b_current": 0,
+    "i_c_current": 0,
+    "instant_apparent_power": 600.0833275470999,
+    "instant_average_voltage": 230.8,
+    "instant_power": -10,
+    "instant_reactive_power": 600,
+    "instant_total_current": -0.4,
+    "last_communication_time": "2020-04-09T05:50:38.990165237-07:00",
+    "timeout": 1500000000
+  },
+  "load": {
+    "energy_exported": 0,
+    "energy_imported": 24751111.13611111,
+    "frequency": 49.99971389770508,
+    "i_a_current": 0,
+    "i_b_current": 0,
+    "i_c_current": 0,
+    "instant_apparent_power": 871.7066645380579,
+    "instant_average_voltage": 232.0439249674479,
+    "instant_power": 734.1549565813606,
+    "instant_reactive_power": -469.988307011022,
+    "instant_total_current": 3.1638620001982423,
+    "last_communication_time": "2020-04-09T05:50:38.974944676-07:00",
+    "timeout": 1500000000
+  },
+  "site": {
+    "energy_exported": 5512641.122754764,
+    "energy_imported": 9852397.795532543,
+    "frequency": 49.99971389770508,
+    "i_a_current": 0,
+    "i_b_current": 0,
+    "i_c_current": 0,
+    "instant_apparent_power": 5388.546173843879,
+    "instant_average_voltage": 232.0439249674479,
+    "instant_power": -5347.455078125,
+    "instant_reactive_power": -664.1942901611328,
+    "instant_total_current": 0,
+    "last_communication_time": "2020-04-09T05:50:38.989687241-07:00",
+    "timeout": 1500000000
+  },
+  "solar": {
+    "energy_exported": 21296639.987777833,
+    "energy_imported": 65.52444450131091,
+    "frequency": 49.95012283325195,
+    "i_a_current": 0,
+    "i_b_current": 0,
+    "i_c_current": 0,
+    "instant_apparent_power": 6113.633873631454,
+    "instant_average_voltage": 232.1537322998047,
+    "instant_power": 6099.032958984375,
+    "instant_reactive_power": -422.27491760253906,
+    "instant_total_current": 0,
+    "last_communication_time": "2020-04-09T05:50:38.974944676-07:00",
+    "timeout": 1500000000
+  }
+}
diff --git a/tests/unit/fixtures/operation.json b/tests/unit/fixtures/operation.json
index d30e46c..5a74de6 100644
--- a/tests/unit/fixtures/operation.json
+++ b/tests/unit/fixtures/operation.json
@@ -1,1 +1,6 @@
-{"real_mode": "self_consumption", "backup_reserve_percent": 5.000019999999999, "freq_shift_load_shed_soe": 0, "freq_shift_load_shed_delta_f": 0}
\ No newline at end of file
+{
+  "backup_reserve_percent": 5.000019999999999,
+  "freq_shift_load_shed_delta_f": 0,
+  "freq_shift_load_shed_soe": 0,
+  "real_mode": "self_consumption"
+}
diff --git a/tests/unit/fixtures/powerwalls.json b/tests/unit/fixtures/powerwalls.json
index 20700dd..6e4a457 100644
--- a/tests/unit/fixtures/powerwalls.json
+++ b/tests/unit/fixtures/powerwalls.json
@@ -1,1 +1,216 @@
-{"enumerating": false, "gateway_din": "gateway_din", "updating": false, "checking_if_offgrid": false, "running_phase_detection": false, "phase_detection_last_error": "phase detection not run", "bubble_shedding": false, "on_grid_check_error": "on grid check not run", "grid_qualifying": false, "grid_code_validating": false, "phase_detection_not_available": true, "powerwalls": [{"Type": "", "PackagePartNumber": "PartNumber1", "PackageSerialNumber": "SerialNumber1", "type": "acpw", "grid_state": "Grid_Uncompliant", "grid_reconnection_time_seconds": 0, "under_phase_detection": false, "updating": false, "commissioning_diagnostic": {"name": "Commissioning", "category": "InternalComms", "disruptive": false, "inputs": null, "checks": [{"name": "CAN connectivity", "status": "fail", "start_time": "2020-10-29T15:02:46.361506132+01:00", "end_time": "2020-10-29T15:02:46.361509132+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Enable switch", "status": "fail", "start_time": "2020-10-29T15:02:46.361511798+01:00", "end_time": "2020-10-29T15:02:46.361513798+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Internal communications", "status": "fail", "start_time": "2020-10-29T15:02:46.361516132+01:00", "end_time": "2020-10-29T15:02:46.361518132+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Firmware up-to-date", "status": "fail", "start_time": "2020-10-29T15:02:46.361520132+01:00", "end_time": "2020-10-29T15:02:46.361522132+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}]}, "update_diagnostic": {"name": "Firmware Update", "category": "InternalComms", "disruptive": true, "inputs": null, "checks": [{"name": "Powerwall firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Battery firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Inverter firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Grid code", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}]}, "bc_type": null}, {"Type": "", "PackagePartNumber": "PartNumber2", "PackageSerialNumber": "SerialNumber2", "type": "acpw", "grid_state": "Grid_Uncompliant", "grid_reconnection_time_seconds": 0, "under_phase_detection": false, "updating": false, "commissioning_diagnostic": {"name": "Commissioning", "category": "InternalComms", "disruptive": false, "inputs": null, "checks": [{"name": "CAN connectivity", "status": "fail", "start_time": "2020-10-29T15:02:46.361754463+01:00", "end_time": "2020-10-29T15:02:46.361757797+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Enable switch", "status": "fail", "start_time": "2020-10-29T15:02:46.36176013+01:00", "end_time": "2020-10-29T15:02:46.36176213+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Internal communications", "status": "fail", "start_time": "2020-10-29T15:02:46.36176413+01:00", "end_time": "2020-10-29T15:02:46.36176713+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}, {"name": "Firmware up-to-date", "status": "fail", "start_time": "2020-10-29T15:02:46.361769463+01:00", "end_time": "2020-10-29T15:02:46.361771463+01:00", "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.", "results": {}, "debug": {}}]}, "update_diagnostic": {"name": "Firmware Update", "category": "InternalComms", "disruptive": true, "inputs": null, "checks": [{"name": "Powerwall firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Battery firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Inverter firmware", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}, {"name": "Grid code", "status": "not_run", "start_time": null, "end_time": null, "progress": 0, "results": null, "debug": null}]}, "bc_type": null}], "has_sync": false, "sync": null, "states": []}
\ No newline at end of file
+{
+  "bubble_shedding": false,
+  "checking_if_offgrid": false,
+  "enumerating": false,
+  "gateway_din": "gateway_din",
+  "grid_code_validating": false,
+  "grid_qualifying": false,
+  "has_sync": false,
+  "on_grid_check_error": "on grid check not run",
+  "phase_detection_last_error": "phase detection not run",
+  "phase_detection_not_available": true,
+  "powerwalls": [
+    {
+      "PackagePartNumber": "PartNumber1",
+      "PackageSerialNumber": "SerialNumber1",
+      "Type": "",
+      "bc_type": null,
+      "commissioning_diagnostic": {
+        "category": "InternalComms",
+        "checks": [
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361509132+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "CAN connectivity",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361506132+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361513798+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Enable switch",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361511798+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361518132+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Internal communications",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361516132+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361522132+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Firmware up-to-date",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361520132+01:00",
+            "status": "fail"
+          }
+        ],
+        "disruptive": false,
+        "inputs": null,
+        "name": "Commissioning"
+      },
+      "grid_reconnection_time_seconds": 0,
+      "grid_state": "Grid_Uncompliant",
+      "type": "acpw",
+      "under_phase_detection": false,
+      "update_diagnostic": {
+        "category": "InternalComms",
+        "checks": [
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Powerwall firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Battery firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Inverter firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Grid code",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          }
+        ],
+        "disruptive": true,
+        "inputs": null,
+        "name": "Firmware Update"
+      },
+      "updating": false
+    },
+    {
+      "PackagePartNumber": "PartNumber2",
+      "PackageSerialNumber": "SerialNumber2",
+      "Type": "",
+      "bc_type": null,
+      "commissioning_diagnostic": {
+        "category": "InternalComms",
+        "checks": [
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361757797+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "CAN connectivity",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361754463+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.36176213+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Enable switch",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.36176013+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.36176713+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Internal communications",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.36176413+01:00",
+            "status": "fail"
+          },
+          {
+            "debug": {},
+            "end_time": "2020-10-29T15:02:46.361771463+01:00",
+            "message": "Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.",
+            "name": "Firmware up-to-date",
+            "results": {},
+            "start_time": "2020-10-29T15:02:46.361769463+01:00",
+            "status": "fail"
+          }
+        ],
+        "disruptive": false,
+        "inputs": null,
+        "name": "Commissioning"
+      },
+      "grid_reconnection_time_seconds": 0,
+      "grid_state": "Grid_Uncompliant",
+      "type": "acpw",
+      "under_phase_detection": false,
+      "update_diagnostic": {
+        "category": "InternalComms",
+        "checks": [
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Powerwall firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Battery firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Inverter firmware",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          },
+          {
+            "debug": null,
+            "end_time": null,
+            "name": "Grid code",
+            "progress": 0,
+            "results": null,
+            "start_time": null,
+            "status": "not_run"
+          }
+        ],
+        "disruptive": true,
+        "inputs": null,
+        "name": "Firmware Update"
+      },
+      "updating": false
+    }
+  ],
+  "running_phase_detection": false,
+  "states": [],
+  "sync": null,
+  "updating": false
+}
diff --git a/tests/unit/fixtures/site_info.json b/tests/unit/fixtures/site_info.json
index 9cb2b3c..bb9e157 100644
--- a/tests/unit/fixtures/site_info.json
+++ b/tests/unit/fixtures/site_info.json
@@ -1,1 +1,22 @@
-{"max_system_energy_kWh": 0, "max_system_power_kW": 0, "site_name": "test", "timezone": "Europe/Berlin", "max_site_meter_power_kW": 1000000000, "min_site_meter_power_kW": -1000000000, "nominal_system_energy_kWh": 27, "nominal_system_power_kW": 10, "grid_code": {"grid_code": "test_grid_code", "grid_voltage_setting": 230, "grid_freq_setting": 50, "grid_phase_setting": "Single", "country": "Germany", "state": "*", "distributor": "*", "utility": "*", "retailer": "*", "region": "test_grid_code_region"}}
\ No newline at end of file
+{
+  "grid_code": {
+    "country": "Germany",
+    "distributor": "*",
+    "grid_code": "test_grid_code",
+    "grid_freq_setting": 50,
+    "grid_phase_setting": "Single",
+    "grid_voltage_setting": 230,
+    "region": "test_grid_code_region",
+    "retailer": "*",
+    "state": "*",
+    "utility": "*"
+  },
+  "max_site_meter_power_kW": 1000000000,
+  "max_system_energy_kWh": 0,
+  "max_system_power_kW": 0,
+  "min_site_meter_power_kW": -1000000000,
+  "nominal_system_energy_kWh": 27,
+  "nominal_system_power_kW": 10,
+  "site_name": "test",
+  "timezone": "Europe/Berlin"
+}
diff --git a/tests/unit/fixtures/sitemaster.json b/tests/unit/fixtures/sitemaster.json
index 795c077..90478da 100644
--- a/tests/unit/fixtures/sitemaster.json
+++ b/tests/unit/fixtures/sitemaster.json
@@ -1,1 +1,6 @@
-{"status": "StatusUp", "running": true, "connected_to_tesla": true, "power_supply_mode": false}
\ No newline at end of file
+{
+  "connected_to_tesla": true,
+  "power_supply_mode": false,
+  "running": true,
+  "status": "StatusUp"
+}
diff --git a/tests/unit/fixtures/status.json b/tests/unit/fixtures/status.json
index c018a20..08a2d0a 100644
--- a/tests/unit/fixtures/status.json
+++ b/tests/unit/fixtures/status.json
@@ -1,1 +1,10 @@
-{"start_time": "2020-10-28 20:14:11 +0800", "up_time_seconds": "17h11m31.214751424s", "is_new": false, "version": "1.50.1 c58c2df3", "git_hash": "d0e69bde519634961cca04a616d2d4dae80b9f61", "commission_count": 0, "device_type": "hec", "sync_type": "v1"}
\ No newline at end of file
+{
+  "commission_count": 0,
+  "device_type": "hec",
+  "git_hash": "d0e69bde519634961cca04a616d2d4dae80b9f61",
+  "is_new": false,
+  "start_time": "2020-10-28 20:14:11 +0800",
+  "sync_type": "v1",
+  "up_time_seconds": "17h11m31.214751424s",
+  "version": "1.50.1 c58c2df3"
+}
diff --git a/tests/unit/fixtures/system_status.json b/tests/unit/fixtures/system_status.json
index 58c94ae..7a1ed61 100644
--- a/tests/unit/fixtures/system_status.json
+++ b/tests/unit/fixtures/system_status.json
@@ -1,1 +1,121 @@
-{"command_source": "Configuration", "battery_target_power": -3646.2544361664613, "battery_target_reactive_power": 0, "nominal_full_pack_energy": 28078, "nominal_energy_remaining": 14807, "max_power_energy_remaining": 0, "max_power_energy_to_be_charged": 0, "max_charge_power": 9200, "max_discharge_power": 9200, "max_apparent_power": 9200.000000000002, "instantaneous_max_discharge_power": 0, "instantaneous_max_charge_power": 0, "grid_services_power": 0, "system_island_state": "SystemGridConnected", "available_blocks": 2, "battery_blocks": [{"Type": "", "PackagePartNumber": "XXX-G", "PackageSerialNumber": "TGXXX", "disabled_reasons": [], "pinv_state": "PINV_GridFollowing", "pinv_grid_state": "Grid_Compliant", "nominal_energy_remaining": 7378, "nominal_full_pack_energy": 14031, "p_out": -1830, "q_out": 30, "v_out": 226.60000000000002, "f_out": 50.067, "i_out": 39, "energy_charged": 5525740, "energy_discharged": 4659550, "off_grid": false, "vf_mode": false, "wobble_detected": false, "charge_power_clamped": false, "backup_ready": true, "OpSeqState": "Active", "version": "67f943cb05d12d"}, {"Type": "", "PackagePartNumber": "XXX-G", "PackageSerialNumber": "TGXXX", "disabled_reasons": [], "pinv_state": "PINV_GridFollowing", "pinv_grid_state": "Grid_Compliant", "nominal_energy_remaining": 7429, "nominal_full_pack_energy": 14047, "p_out": -1830, "q_out": 30, "v_out": 230, "f_out": 50.068, "i_out": 39.2, "energy_charged": 5547410, "energy_discharged": 4677070, "off_grid": false, "vf_mode": false, "wobble_detected": false, "charge_power_clamped": false, "backup_ready": true, "OpSeqState": "Active", "version": "67f943cb05d12d"}], "ffr_power_availability_high": 9200, "ffr_power_availability_low": 9200, "load_charge_constraint": 0, "max_sustained_ramp_rate": 2500000, "grid_faults": [{"timestamp": 1634015591828, "alert_name": "PINV_a008_vfCheckRocof", "alert_is_fault": false, "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a008_vfCheckRocof\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"}]", "alert_raw": 576460752303423488, "git_hash": "67f943cb05d12d", "site_uid": "TG-XXX", "ecu_type": "TEPINV", "ecu_package_part_number": "XXX-J", "ecu_package_serial_number": "TXXX"}, {"timestamp": 1634015591733, "alert_name": "PINV_a007_vfCheckOverFrequency", "alert_is_fault": false, "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a007_vfCheckOverFrequency\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"},{\"name\":\"PINV_a007_frequency\",\"value\":52.189,\"units\":\"Hz\"}]", "alert_raw": 504575983454519296, "git_hash": "67f943cb05d12d", "site_uid": "XXX-uid", "ecu_type": "TEPINV", "ecu_package_part_number": "XXX-J", "ecu_package_serial_number": "TXXX"}, {"timestamp": 1634015591646, "alert_name": "PINV_a004_vfCheckUnderVoltage", "alert_is_fault": false, "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a004_vfCheckUnderVoltage\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"},{\"name\":\"PINV_a004_uv_amplitude\",\"value\":123,\"units\":\"Vrms\"}]", "alert_raw": 288365616081928192, "git_hash": "67f943cb05d12d", "site_uid": "TG-XXX", "ecu_type": "TEPINV", "ecu_package_part_number": "1081100-79-J", "ecu_package_serial_number": "TXXX"}], "can_reboot": "Power flow is too high", "smart_inv_delta_p": 0, "smart_inv_delta_q": 0, "last_toggle_timestamp": "2021-09-30T18:11:41.110543639+02:00", "solar_real_power_limit": -1, "score": 10000, "blocks_controlled": 2, "primary": true, "auxiliary_load": 0, "all_enable_lines_high": true, "inverter_nominal_usable_power": 9200, "expected_energy_remaining": 0}
+{
+  "all_enable_lines_high": true,
+  "auxiliary_load": 0,
+  "available_blocks": 2,
+  "battery_blocks": [
+    {
+      "OpSeqState": "Active",
+      "PackagePartNumber": "XXX-G",
+      "PackageSerialNumber": "TGXXX",
+      "Type": "",
+      "backup_ready": true,
+      "charge_power_clamped": false,
+      "disabled_reasons": [],
+      "energy_charged": 5525740,
+      "energy_discharged": 4659550,
+      "f_out": 50.067,
+      "i_out": 39,
+      "nominal_energy_remaining": 7378,
+      "nominal_full_pack_energy": 14031,
+      "off_grid": false,
+      "p_out": -1830,
+      "pinv_grid_state": "Grid_Compliant",
+      "pinv_state": "PINV_GridFollowing",
+      "q_out": 30,
+      "v_out": 226.60000000000002,
+      "version": "67f943cb05d12d",
+      "vf_mode": false,
+      "wobble_detected": false
+    },
+    {
+      "OpSeqState": "Active",
+      "PackagePartNumber": "XXX-G",
+      "PackageSerialNumber": "TGXXX",
+      "Type": "",
+      "backup_ready": true,
+      "charge_power_clamped": false,
+      "disabled_reasons": [],
+      "energy_charged": 5547410,
+      "energy_discharged": 4677070,
+      "f_out": 50.068,
+      "i_out": 39.2,
+      "nominal_energy_remaining": 7429,
+      "nominal_full_pack_energy": 14047,
+      "off_grid": false,
+      "p_out": -1830,
+      "pinv_grid_state": "Grid_Compliant",
+      "pinv_state": "PINV_GridFollowing",
+      "q_out": 30,
+      "v_out": 230,
+      "version": "67f943cb05d12d",
+      "vf_mode": false,
+      "wobble_detected": false
+    }
+  ],
+  "battery_target_power": -3646.2544361664613,
+  "battery_target_reactive_power": 0,
+  "blocks_controlled": 2,
+  "can_reboot": "Power flow is too high",
+  "command_source": "Configuration",
+  "expected_energy_remaining": 0,
+  "ffr_power_availability_high": 9200,
+  "ffr_power_availability_low": 9200,
+  "grid_faults": [
+    {
+      "alert_is_fault": false,
+      "alert_name": "PINV_a008_vfCheckRocof",
+      "alert_raw": 576460752303423488,
+      "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a008_vfCheckRocof\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"}]",
+      "ecu_package_part_number": "XXX-J",
+      "ecu_package_serial_number": "TXXX",
+      "ecu_type": "TEPINV",
+      "git_hash": "67f943cb05d12d",
+      "site_uid": "TG-XXX",
+      "timestamp": 1634015591828
+    },
+    {
+      "alert_is_fault": false,
+      "alert_name": "PINV_a007_vfCheckOverFrequency",
+      "alert_raw": 504575983454519296,
+      "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a007_vfCheckOverFrequency\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"},{\"name\":\"PINV_a007_frequency\",\"value\":52.189,\"units\":\"Hz\"}]",
+      "ecu_package_part_number": "XXX-J",
+      "ecu_package_serial_number": "TXXX",
+      "ecu_type": "TEPINV",
+      "git_hash": "67f943cb05d12d",
+      "site_uid": "XXX-uid",
+      "timestamp": 1634015591733
+    },
+    {
+      "alert_is_fault": false,
+      "alert_name": "PINV_a004_vfCheckUnderVoltage",
+      "alert_raw": 288365616081928192,
+      "decoded_alert": "[{\"name\":\"PINV_alertID\",\"value\":\"PINV_a004_vfCheckUnderVoltage\"},{\"name\":\"PINV_alertType\",\"value\":\"Warning\"},{\"name\":\"PINV_a004_uv_amplitude\",\"value\":123,\"units\":\"Vrms\"}]",
+      "ecu_package_part_number": "1081100-79-J",
+      "ecu_package_serial_number": "TXXX",
+      "ecu_type": "TEPINV",
+      "git_hash": "67f943cb05d12d",
+      "site_uid": "TG-XXX",
+      "timestamp": 1634015591646
+    }
+  ],
+  "grid_services_power": 0,
+  "instantaneous_max_charge_power": 0,
+  "instantaneous_max_discharge_power": 0,
+  "inverter_nominal_usable_power": 9200,
+  "last_toggle_timestamp": "2021-09-30T18:11:41.110543639+02:00",
+  "load_charge_constraint": 0,
+  "max_apparent_power": 9200.000000000002,
+  "max_charge_power": 9200,
+  "max_discharge_power": 9200,
+  "max_power_energy_remaining": 0,
+  "max_power_energy_to_be_charged": 0,
+  "max_sustained_ramp_rate": 2500000,
+  "nominal_energy_remaining": 14807,
+  "nominal_full_pack_energy": 28078,
+  "primary": true,
+  "score": 10000,
+  "smart_inv_delta_p": 0,
+  "smart_inv_delta_q": 0,
+  "solar_real_power_limit": -1,
+  "system_island_state": "SystemGridConnected"
+}
diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py
index 70f1c91..3558cb9 100644
--- a/tests/unit/test_api.py
+++ b/tests/unit/test_api.py
@@ -5,7 +5,7 @@
 import responses
 from responses import GET, Response, add
 
-from tesla_powerwall import API, AccessDeniedError, APIError
+from tesla_powerwall import API, AccessDeniedError, ApiError
 from tests.unit import ENDPOINT
 
 
@@ -33,20 +33,20 @@ def test_process_response(self):
             self.api._process_response(res)
 
         res.status_code = 404
-        with self.assertRaises(APIError):
+        with self.assertRaises(ApiError):
             self.api._process_response(res)
 
         res.status_code = 502
-        with self.assertRaises(APIError):
+        with self.assertRaises(ApiError):
             self.api._process_response(res)
 
         res.status_code = 200
         res._content = b'{"error": "test_error"}'
-        with self.assertRaises(APIError):
+        with self.assertRaises(ApiError):
             self.api._process_response(res)
 
         res._content = b'{invalid_json"'
-        with self.assertRaises(APIError):
+        with self.assertRaises(ApiError):
             self.api._process_response(res)
 
         res._content = b"{}"
@@ -86,3 +86,14 @@ def test_is_authenticated(self):
 
     def test_url(self):
         self.assertEqual(self.api.url("test"), ENDPOINT + "test")
+
+    @responses.activate
+    def test_logout(self):
+        add(
+            Response(GET, url=f"{ENDPOINT}logout"),
+            body="",
+            content_type="application/json",
+        )
+        self.api._http_session.cookies.set("AuthCookie", "foo")
+
+        self.api.logout()
diff --git a/tests/unit/test_powerwall.py b/tests/unit/test_powerwall.py
index 9b01977..381fe2d 100644
--- a/tests/unit/test_powerwall.py
+++ b/tests/unit/test_powerwall.py
@@ -9,13 +9,15 @@
     DeviceType,
     GridStatus,
     IslandMode,
-    Meter,
+    MeterDetailsReadings,
+    MeterDetailsResponse,
     MeterNotAvailableError,
-    MetersAggregates,
+    MeterResponse,
+    MetersAggregatesResponse,
     MeterType,
     MissingAttributeError,
     Powerwall,
-    SiteMaster,
+    SiteMasterResponse,
     assert_attribute,
     convert_to_kw,
 )
@@ -23,6 +25,10 @@
 from tests.unit import (
     ENDPOINT,
     GRID_STATUS_RESPONSE,
+    ISLANDING_MODE_OFFGRID_RESPONSE,
+    ISLANDING_MODE_ONGRID_RESPONSE,
+    METER_SITE_RESPONSE,
+    METER_SOLAR_RESPONSE,
     METERS_AGGREGATES_RESPONSE,
     OPERATION_RESPONSE,
     POWERWALLS_RESPONSE,
@@ -30,8 +36,6 @@
     SITEMASTER_RESPONSE,
     STATUS_RESPONSE,
     SYSTEM_STATUS_RESPONSE,
-    ISLANDING_MODE_ONGRID_RESPONSE,
-    ISLANDING_MODE_OFFGRID_RESPONSE,
 )
 
 
@@ -46,7 +50,9 @@ def test_get_api(self):
     def test_get_charge(self):
         add(
             Response(
-                GET, url=f"{ENDPOINT}system_status/soe", json={"percentage": 53.123423}
+                GET,
+                url=f"{ENDPOINT}system_status/soe",
+                json={"percentage": 53.123423},
             )
         )
         self.assertEqual(self.powerwall.get_charge(), 53.123423)
@@ -55,12 +61,14 @@ def test_get_charge(self):
     def test_get_sitemaster(self):
         add(
             Response(
-                responses.GET, url=f"{ENDPOINT}sitemaster", json=SITEMASTER_RESPONSE
+                responses.GET,
+                url=f"{ENDPOINT}sitemaster",
+                json=SITEMASTER_RESPONSE,
             )
         )
 
         sitemaster = self.powerwall.get_sitemaster()
-        self.assertIsInstance(sitemaster, SiteMaster)
+        self.assertIsInstance(sitemaster, SiteMasterResponse)
 
         self.assertEqual(sitemaster.status, "StatusUp")
         self.assertEqual(sitemaster.is_running, True)
@@ -77,16 +85,63 @@ def test_get_meters(self):
             )
         )
         meters = self.powerwall.get_meters()
-        self.assertIsInstance(meters, MetersAggregates)
+        self.assertIsInstance(meters, MetersAggregatesResponse)
         self.assertListEqual(
-            meters.meters,
-            [MeterType.SITE, MeterType.BATTERY, MeterType.LOAD, MeterType.SOLAR],
+            list(meters.meters.keys()),
+            [
+                MeterType.BATTERY,
+                MeterType.LOAD,
+                MeterType.SITE,
+                MeterType.SOLAR,
+            ],
         )
-        self.assertIsInstance(meters.load, Meter)
-        self.assertIsInstance(meters.get_meter(MeterType.LOAD), Meter)
+        self.assertIsInstance(meters.load, MeterResponse)
+        self.assertIsInstance(meters.get_meter(MeterType.LOAD), MeterResponse)
+        self.assertIsNone(meters.get_meter(MeterType.GENERATOR))
         with self.assertRaises(MeterNotAvailableError):
             meters.generator
 
+    @responses.activate
+    def test_get_meter_site(self):
+        add(
+            Response(
+                responses.GET,
+                url=f"{ENDPOINT}meters/site",
+                json=METER_SITE_RESPONSE,
+            )
+        )
+        meter = self.powerwall.get_meter_site()
+        self.assertIsInstance(meter, MeterDetailsResponse)
+        self.assertEqual(meter.location, MeterType.SITE)
+        readings = meter.readings
+        self.assertIsInstance(readings, MeterDetailsReadings)
+        # Optional voltage fields
+        self.assertIsInstance(readings.v_l1n, float)
+        self.assertIsInstance(readings.v_l2n, float)
+        self.assertIsNone(readings.v_l3n)
+
+        self.assertEqual(readings.instant_power, -18.00000076368451)
+        self.assertEqual(readings.get_power(), -0.0)
+
+    @responses.activate
+    def test_get_meter_solar(self):
+        add(
+            Response(
+                responses.GET,
+                url=f"{ENDPOINT}meters/solar",
+                json=METER_SOLAR_RESPONSE,
+            )
+        )
+        meter = self.powerwall.get_meter_solar()
+        self.assertIsInstance(meter, MeterDetailsResponse)
+        self.assertEqual(meter.location, MeterType.SOLAR)
+        readings = meter.readings
+        self.assertIsInstance(readings, MeterDetailsReadings)
+        # Optional voltage fields
+        self.assertIsInstance(readings.v_l1n, float)
+        self.assertIsNone(readings.v_l2n)
+        self.assertIsNone(readings.v_l3n)
+
     @responses.activate
     def test_is_sending(self):
         add(
@@ -131,7 +186,11 @@ def test_is_grid_services_active(self):
     @responses.activate
     def test_get_site_info(self):
         add(
-            Response(responses.GET, url=f"{ENDPOINT}site_info", json=SITE_INFO_RESPONSE)
+            Response(
+                responses.GET,
+                url=f"{ENDPOINT}site_info",
+                json=SITE_INFO_RESPONSE,
+            )
         )
         site_info = self.powerwall.get_site_info()
         self.assertEqual(site_info.nominal_system_energy, 27)
@@ -172,7 +231,9 @@ def test_get_device_type(self):
     def test_get_serial_numbers(self):
         add(
             Response(
-                responses.GET, url=f"{ENDPOINT}powerwalls", json=POWERWALLS_RESPONSE
+                responses.GET,
+                url=f"{ENDPOINT}powerwalls",
+                json=POWERWALLS_RESPONSE,
             )
         )
         serial_numbers = self.powerwall.get_serial_numbers()
@@ -182,7 +243,9 @@ def test_get_serial_numbers(self):
     def test_get_gateway_din(self):
         add(
             Response(
-                responses.GET, url=f"{ENDPOINT}powerwalls", json=POWERWALLS_RESPONSE
+                responses.GET,
+                url=f"{ENDPOINT}powerwalls",
+                json=POWERWALLS_RESPONSE,
             )
         )
         gateway_din = self.powerwall.get_gateway_din()
@@ -191,7 +254,11 @@ def test_get_gateway_din(self):
     @responses.activate
     def test_get_backup_reserved_percentage(self):
         add(
-            Response(responses.GET, url=f"{ENDPOINT}operation", json=OPERATION_RESPONSE)
+            Response(
+                responses.GET,
+                url=f"{ENDPOINT}operation",
+                json=OPERATION_RESPONSE,
+            )
         )
         self.assertEqual(
             self.powerwall.get_backup_reserve_percentage(), 5.000019999999999
@@ -200,7 +267,11 @@ def test_get_backup_reserved_percentage(self):
     @responses.activate
     def test_get_operation_mode(self):
         add(
-            Response(responses.GET, url=f"{ENDPOINT}operation", json=OPERATION_RESPONSE)
+            Response(
+                responses.GET,
+                url=f"{ENDPOINT}operation",
+                json=OPERATION_RESPONSE,
+            )
         )
         self.assertEqual(
             self.powerwall.get_operation_mode(), OperationMode.SELF_CONSUMPTION
@@ -242,7 +313,7 @@ def test_islanding_mode_offgrid(self):
                 json=ISLANDING_MODE_OFFGRID_RESPONSE,
             )
         )
-        
+
         mode = self.powerwall.set_island_mode(IslandMode.OFFGRID)
         self.assertEqual(mode, IslandMode.OFFGRID)
 
@@ -255,7 +326,7 @@ def test_islanding_mode_ongrid(self):
                 json=ISLANDING_MODE_ONGRID_RESPONSE,
             )
         )
-        
+
         mode = self.powerwall.set_island_mode(IslandMode.ONGRID)
         self.assertEqual(mode, IslandMode.ONGRID)