diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml
index b1b96ce3..4716e0e1 100644
--- a/.devcontainer/configuration.yaml
+++ b/.devcontainer/configuration.yaml
@@ -8,4 +8,4 @@ logger:
authcaptureproxy: debug
teslajsonpy: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
-debugpy:
+# debugpy:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 37160cba..b51ce99f 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,9 +1,10 @@
{
- "image": "ludeeus/container:integration-debian",
"name": "Tesla Custom Component development",
- "context": "..",
- "appPort": ["9123:8123"],
- "postCreateCommand": "container install && pip install poetry && poetry config virtualenvs.create false && poetry install",
+ "image": "ghcr.io/ludeeus/devcontainer/integration:stable",
+ "appPort": [
+ "9123:8123"
+ ],
+ "postCreateCommand": "bash .devcontainer/post-create.sh",
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
@@ -29,4 +30,4 @@
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
-}
+}
\ No newline at end of file
diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh
new file mode 100644
index 00000000..a414b2d6
--- /dev/null
+++ b/.devcontainer/post-create.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+source /opt/container/helpers/common/paths.sh
+mkdir -p /config
+
+# Required to get automations to work
+echo "Creating automations.yaml"
+touch /config/automations.yaml
+
+# source: /opt/container/helpers/commons/homeassistant/start.sh
+if test -d "custom_components"; then
+ echo "Symlink the custom component directory"
+
+ if test -d "custom_components"; then
+ rm -R /config/custom_components
+ fi
+
+ ln -sf "$(workspacePath)custom_components/" /config/custom_components || echo "Could not copy the custom_component" exit 1
+elif test -f "__init__.py"; then
+ echo "Having the component in the root is currently not supported"
+fi
+
+# Install
+echo "Install home assistant"
+container install
+
+# Setup the Dev Stuff
+
+pip install poetry
+# We're in Docker, so we don't need a VENV
+poetry config virtualenvs.create false
+poetry install --no-interaction
+
+# Keep this inline with any requirements that are in manifest.json
+pip install git+https://github.com/zabuldon/teslajsonpy.git@dev#teslajsonpy==2.2.0
diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml
index 977daf4c..0ca6ba75 100644
--- a/.github/workflows/pull.yml
+++ b/.github/workflows/pull.yml
@@ -36,10 +36,10 @@ jobs:
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- - name: Set up Python 3.9
+ - name: Set up Python 3.10
uses: actions/setup-python@v2
with:
- python-version: 3.9
+ python-version: "3.10"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index df20f271..0c33b1f1 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -38,10 +38,10 @@ jobs:
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- - name: Set up Python 3.9
+ - name: Set up Python 3.10
uses: actions/setup-python@v2
with:
- python-version: 3.9
+ python-version: "3.10"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
diff --git a/.gitignore b/.gitignore
index c86115c7..fd94db61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,10 @@ hs_err_pid*
*.jl.mem
deps/deps.jl
+# User settings
+/.vscode/settings.json
+
+
# Keep these config files
!/.gitignore
!/.travis.yml
@@ -72,3 +76,4 @@ deps/deps.jl
!/.scrutinizer.yml
!/.prospector.yml
!/.devcontainer
+!/.vscode
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..240488f7
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,35 @@
+{
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // Example of attaching to local debug server
+ "name": "Python: Attach Local",
+ "type": "python",
+ "request": "attach",
+ "port": 5678,
+ "justMyCode": false,
+ "host": "localhost",
+ "pathMappings": [
+ {
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "."
+ }
+ ]
+ },
+ {
+ // Example of attaching to my production server
+ "name": "Python: Attach Remote",
+ "type": "python",
+ "request": "attach",
+ "port": 5678,
+ "host": "homeassistant.local",
+ "pathMappings": [
+ {
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "/usr/src/homeassistant"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json
new file mode 100644
index 00000000..386af462
--- /dev/null
+++ b/.vscode/settings.default.json
@@ -0,0 +1,10 @@
+{
+ "python.testing.pytestArgs": [
+ "tests"
+ ],
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "files.associations": {
+ "*.yaml": "home-assistant"
+ }
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..a4ae2df5
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,35 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Run Home Assistant on port 9123",
+ "type": "shell",
+ "command": "container start",
+ "problemMatcher": []
+ },
+ {
+ "label": "Run Home Assistant configuration against /config",
+ "type": "shell",
+ "command": "container check",
+ "problemMatcher": []
+ },
+ {
+ "label": "Upgrade Home Assistant to latest dev",
+ "type": "shell",
+ "command": "container install",
+ "problemMatcher": []
+ },
+ {
+ "label": "Install a specific version of Home Assistant",
+ "type": "shell",
+ "command": "container set-version",
+ "problemMatcher": []
+ },
+ {
+ "label": "Serve Documentation on port 8000",
+ "type": "shell",
+ "command": "mkdocs -v serve",
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 6f080ba4..eda32bf5 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,7 @@
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]
-A fork of the [official Tesla integration](https://www.home-assistant.io/integrations/tesla/) in Home Assistant.
-
-This is the successor to the core app which was removed due to Tesla login issues. Do not report issues to Home Assistant.
+A fork of the previous official Tesla integration in Home Assistant which has been removed due to Tesla login issues. Do not report issues to Home Assistant.
To use the component, you will need an application to generate a Tesla refresh token:
@@ -36,32 +34,36 @@ To use the component, you will need an application to generate a Tesla refresh t
7. Restart Home Assistant.
8. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Tesla Custom Integration".
+Note: This integration will wake up your vehicle(s) during installation.
+
## Usage
The `Tesla` integration offers integration with the [Tesla](https://auth.tesla.com/login) cloud service and provides presence detection as well as sensors such as charger state and temperature.
-This integration provides the following platforms:
+This integration provides the following entities for vehicles:
+
+- Binary sensors - charger connection, charging status, car online and parking brake.
+- Buttons - horn, flash lights, wake up1, force data update1 and trigger HomeLink. **Note:** The HomeLink button is disabled by default as some vehicles don't have this option. Enable via configuration/entities if desired.
+- Climate - turn HVAC on/off, set target temperature, set preset modes (defrost, keep on, dog mode and camp mode).
+- Device tracker - car location1.
+- Locks - door lock, rear trunk lock, front trunk (frunk) lock and charger door lock.
+**Note:** Set `state` to `heat_cool` or `off` to enable/disable your Tesla's climate system via a scene.
+- Selects - seat heaters and cabin overheat protection2.
+- Sensors - battery level, charge rate, energy added, inside/outside temperature, odometer and estimated range.
+- Switches - heated steering wheel, charger, sentry mode and polling1.
+- Update - software update2
-- Binary sensors - such as update available, parking, and charger connection.
-- Sensors - such as Battery level, Inside/Outside temperature, odometer, estimated range, charging rate, and vehicle data
-- Device tracker - to track location of your car
-- Locks - Door lock, rear trunk lock, front trunk (frunk) lock and charger door lock. Enables you to control Tesla's door, trunks and charger door lock.
-- Climate - HVAC control. Allow you to control (turn on/off, set target temperature) your Tesla's HVAC system. Also enables preset modes to enable or disable max defrost mode `defrost` or `normal` operation mode. **NOTE:** Set `state` to `heat_cool` or `off` to enable/disable your Tesla's climate system via a scene.
-- Switches - Charger and max range switch allow you to start/stop charging and set max range charging. Polling switch allows you to disable polling of vehicles to conserve battery. Sentry mode switch enables or disable Sentry mode.
-- Buttons - Horn, Flash lights, and Trigger homelink. **Note:** The homelink button is disabled by default as many vehicles don't have the homelink option. Enable via configuration/entities if desired.
+1 *Diagnostics entities.*
+2 *Configuration entities.*
-The following sensors provide all available vehicle data as attributes. These sensors are disabled by default and need to be enabled in HASS first. It is also recommended to exclude these sensors from [recorder](https://www.home-assistant.io/integrations/recorder/).
-- Climate data sensor
-- Charge data sensor
-- Vehicle state data sensor
-- Software update data sensor
-- Speed Limit data sensor
-- Vehicle Config data sensor
-- Drive State data sensor
-- GUI Settings data sensor
+This integration provides the following entities for energy sites:
+
+- Binary sensors - Powerwall charging and grid status.
+- Selects - grid charging, export rule and operation mode.
+- Sensors - solar power, grid power, load power, battery level, battery Wh remaining and backup reserve.
This integration provies the following platforms for solar systems:
@@ -72,9 +74,7 @@ This integration provies the following platforms for solar systems:
Tesla options are set via **Configuration** -> **Integrations** -> **Tesla** -> **Options**.
- Seconds between polling - referred to below as the `polling_interval`.
-
- Wake cars on start - Whether to wake sleeping cars on Home Assistant startup. This allows a user to choose whether cars should continue to sleep (and not update information) or to wake up the cars potentially interrupting long term hibernation and increasing vampire drain.
-
- Polling policy - When do we actively poll the car to get updates, and when do we try to allow the car to sleep. See [the Wiki](https://github.com/alandtse/tesla/wiki/Polling-policy) for more information.
## Potential Battery impacts
diff --git a/custom_components/tesla_custom/__init__.py b/custom_components/tesla_custom/__init__.py
index 2de42264..ad5632b8 100644
--- a/custom_components/tesla_custom/__init__.py
+++ b/custom_components/tesla_custom/__init__.py
@@ -1,12 +1,11 @@
"""Support for Tesla cars."""
import asyncio
-from collections import defaultdict
from datetime import timedelta
from http import HTTPStatus
import logging
import async_timeout
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_DOMAIN,
@@ -15,9 +14,8 @@
CONF_USERNAME,
EVENT_HOMEASSISTANT_CLOSE,
)
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import httpx
@@ -28,6 +26,8 @@
from .config_flow import CannotConnect, InvalidAuth, validate_input
from .const import (
CONF_EXPIRATION,
+ CONF_INCLUDE_VEHICLES,
+ CONF_INCLUDE_ENERGYSITES,
CONF_POLLING_POLICY,
CONF_WAKE_ON_START,
DATA_LISTENER,
@@ -39,7 +39,6 @@
PLATFORMS,
)
from .services import async_setup_services, async_unload_services
-from .tesla_device import device_identifier
_LOGGER = logging.getLogger(__name__)
@@ -83,11 +82,14 @@ def _update_entry(email, data=None, options=None):
hass.config_entries.async_update_entry(entry, data=data, options=options)
config = base_config.get(DOMAIN)
+
if not config:
return True
+
email = config[CONF_USERNAME]
token = config[CONF_TOKEN]
scan_interval = config[CONF_SCAN_INTERVAL]
+
if email in _async_configured_emails(hass):
try:
info = await validate_input(hass, config)
@@ -113,6 +115,7 @@ def _update_entry(email, data=None, options=None):
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][email] = {CONF_SCAN_INTERVAL: scan_interval}
+
return True
@@ -121,17 +124,21 @@ async def async_setup_entry(hass, config_entry):
# pylint: disable=too-many-locals
hass.data.setdefault(DOMAIN, {})
config = config_entry.data
- # Because users can have multiple accounts, we always create a new session so they have separate cookies
+ # Because users can have multiple accounts, we always
+ # create a new session so they have separate cookies
async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}, timeout=60)
email = config_entry.title
+
if not hass.data[DOMAIN]:
async_setup_services(hass)
+
if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]:
scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL]
hass.config_entries.async_update_entry(
config_entry, options={CONF_SCAN_INTERVAL: scan_interval}
)
hass.data[DOMAIN].pop(email)
+
try:
controller = TeslaAPI(
async_client,
@@ -148,33 +155,37 @@ async def async_setup_entry(hass, config_entry):
),
)
result = await controller.connect(
- wake_if_asleep=config_entry.options.get(
- CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START
- )
+ include_vehicles=config.get(CONF_INCLUDE_VEHICLES),
+ include_energysites=config.get(CONF_INCLUDE_ENERGYSITES),
)
refresh_token = result["refresh_token"]
access_token = result["access_token"]
expiration = result["expiration"]
+
except IncompleteCredentials as ex:
await async_client.aclose()
raise ConfigEntryAuthFailed from ex
- except httpx.ConnectTimeout as ex:
+
+ except (httpx.ConnectTimeout, httpx.ConnectError) as ex:
await async_client.aclose()
raise ConfigEntryNotReady from ex
+
except TeslaException as ex:
await async_client.aclose()
+
if ex.code == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from ex
+
if ex.message in [
- "VEHICLE_UNAVAILABLE",
"TOO_MANY_REQUESTS",
- "SERVICE_MAINTENANCE",
"UPSTREAM_TIMEOUT",
]:
raise ConfigEntryNotReady(
f"Temporarily unable to communicate with Tesla API: {ex.message}"
) from ex
+
_LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
+
return False
async def _async_close_client(*_):
@@ -193,24 +204,66 @@ def _async_create_close_task():
coordinator = TeslaDataUpdateCoordinator(
hass, config_entry=config_entry, controller=controller
)
- # Fetch initial data so we have data when entities subscribe
- entry_data = hass.data[DOMAIN][config_entry.entry_id] = {
+
+ try:
+ if config_entry.data.get("initial_setup"):
+ wake_if_asleep = True
+ else:
+ wake_if_asleep = config_entry.options.get(
+ CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START
+ )
+
+ cars = await controller.generate_car_objects(wake_if_asleep=wake_if_asleep)
+
+ hass.config_entries.async_update_entry(
+ config_entry, data={**config_entry.data, "initial_setup": False}
+ )
+
+ except TeslaException as ex:
+ await async_client.aclose()
+
+ if ex.message in [
+ "TOO_MANY_REQUESTS",
+ "SERVICE_MAINTENANCE",
+ "UPSTREAM_TIMEOUT",
+ ]:
+ raise ConfigEntryNotReady(
+ f"Temporarily unable to communicate with Tesla API: {ex.message}"
+ ) from ex
+
+ _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
+
+ return False
+
+ try:
+ energysites = await controller.generate_energysite_objects()
+
+ except TeslaException as ex:
+ await async_client.aclose()
+
+ if ex.message in [
+ "TOO_MANY_REQUESTS",
+ "SERVICE_MAINTENANCE",
+ "UPSTREAM_TIMEOUT",
+ ]:
+ raise ConfigEntryNotReady(
+ f"Temporarily unable to communicate with Tesla API: {ex.message}"
+ ) from ex
+
+ _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
+
+ return False
+
+ hass.data[DOMAIN][config_entry.entry_id] = {
"coordinator": coordinator,
- "devices": defaultdict(list),
+ "cars": cars,
+ "energysites": energysites,
DATA_LISTENER: [config_entry.add_update_listener(update_listener)],
}
_LOGGER.debug("Connected to the Tesla API")
await coordinator.async_config_entry_first_refresh()
- all_devices = controller.get_homeassistant_components()
-
- if not all_devices:
- return False
-
- for device in all_devices:
- entry_data["devices"][device.hass_type].append(device)
-
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
return True
@@ -224,15 +277,20 @@ async def async_unload_entry(hass, config_entry) -> bool:
await hass.data[DOMAIN].get(config_entry.entry_id)[
"coordinator"
].controller.disconnect()
+
for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]:
listener()
username = config_entry.title
+
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
_LOGGER.debug("Unloaded entry for %s", username)
+
if not hass.data[DOMAIN]:
async_unload_services(hass)
+
return True
+
return False
@@ -254,7 +312,7 @@ async def update_listener(hass, config_entry):
class TeslaDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Tesla data."""
- def __init__(self, hass, *, config_entry, controller):
+ def __init__(self, hass, *, config_entry, controller: TeslaAPI):
"""Initialize global Tesla data updater."""
self.controller = controller
self.config_entry = config_entry
@@ -290,16 +348,3 @@ async def _async_update_data(self):
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
except TeslaException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
-
-
-async def async_remove_config_entry_device(
- hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
-) -> bool:
- """Remove tesla_custom config entry from a device."""
- controller: TeslaAPI = hass.data[DOMAIN][config_entry.entry_id][
- "coordinator"
- ].controller
- return not device_entry.identifiers.intersection(
- device_identifier(telsa_device)
- for telsa_device in controller.get_homeassistant_components()
- )
diff --git a/custom_components/tesla_custom/base.py b/custom_components/tesla_custom/base.py
new file mode 100644
index 00000000..c1f77649
--- /dev/null
+++ b/custom_components/tesla_custom/base.py
@@ -0,0 +1,170 @@
+"""Support for Tesla cars and energy sites."""
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.const import RESOURCE_TYPE_BATTERY
+from teslajsonpy.energy import EnergySite
+
+from homeassistant.const import CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.util import slugify
+
+from . import TeslaDataUpdateCoordinator
+from .const import ATTRIBUTION, DOMAIN
+
+
+class TeslaBaseEntity(CoordinatorEntity):
+ """Representation of a Tesla device."""
+
+ _attr_attribution = ATTRIBUTION
+ _attr_has_entity_name = True
+
+ def __init__(
+ self, hass: HomeAssistant, coordinator: TeslaDataUpdateCoordinator
+ ) -> None:
+ """Initialise the Tesla device."""
+ super().__init__(coordinator)
+ self._coordinator = coordinator
+ self._enabled_by_default: bool = True
+ self.hass = hass
+ self.type = None
+
+ def refresh(self) -> None:
+ """Refresh the device data.
+
+ This is called by the DataUpdateCoodinator when new data is available.
+
+ This assumes the controller has already been updated. This should be
+ called by inherited classes so the overall device information is updated.
+ """
+ self.async_write_ha_state()
+
+ @property
+ def name(self) -> str:
+ """Return device name."""
+ return self.type.capitalize()
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Set entity registry to default."""
+ return self._enabled_by_default
+
+ async def async_added_to_hass(self) -> None:
+ """Register state update callback."""
+ self.async_on_remove(self.coordinator.async_add_listener(self.refresh))
+
+
+class TeslaCarEntity(TeslaBaseEntity):
+ """Representation of a Tesla car device."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialise the Tesla car device."""
+ super().__init__(hass, coordinator)
+ self._car = car
+ self._unit_system = (
+ CONF_UNIT_SYSTEM_METRIC
+ if self.hass.config.units.is_metric
+ else CONF_UNIT_SYSTEM_IMPERIAL
+ )
+
+ async def update_controller(
+ self, *, wake_if_asleep: bool = False, force: bool = True, blocking: bool = True
+ ) -> None:
+ """Get the latest data from Tesla.
+
+ This does a controller update then a coordinator update.
+ The coordinator triggers a call to the refresh function.
+
+ Setting the blocking param to False will create a background task for the update.
+ """
+
+ if blocking is False:
+ await self.hass.async_create_task(
+ self.update_controller(wake_if_asleep=wake_if_asleep, force=force)
+ )
+ return
+
+ await self._coordinator.controller.update(
+ self._car.id, wake_if_asleep=wake_if_asleep, force=force
+ )
+ await self._coordinator.async_refresh()
+
+ @property
+ def vehicle_name(self) -> str:
+ """Return vehicle name."""
+ return (
+ self._car.display_name
+ if self._car.display_name is not None
+ and self._car.display_name != self._car.vin[-6:]
+ else f"Tesla Model {str(self._car.vin[3]).upper()}"
+ )
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique id for car entity."""
+ return slugify(f"{self._car.vin} {self.type}")
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device info."""
+ return DeviceInfo(
+ identifiers={(DOMAIN, self._car.id)},
+ name=self.vehicle_name,
+ manufacturer="Tesla",
+ model=self._car.car_type,
+ sw_version=self._car.car_version,
+ )
+
+ @property
+ def assumed_state(self) -> bool:
+ # pylint: disable=protected-access
+ """Return whether the data is from an online vehicle."""
+ return not self._coordinator.controller.is_car_online(vin=self._car.vin) and (
+ self._coordinator.controller.get_last_update_time(vin=self._car.vin)
+ - self._coordinator.controller.get_last_wake_up_time(vin=self._car.vin)
+ > self._coordinator.controller.update_interval
+ )
+
+
+class TeslaEnergyEntity(TeslaBaseEntity):
+ """Representation of a Tesla energy device."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: EnergySite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialise the Tesla energy device."""
+ super().__init__(hass, coordinator)
+ self._energysite = energysite
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique id for energy site device."""
+ return slugify(f"{self._energysite.energysite_id} {self.type}")
+
+ @property
+ def sw_version(self) -> bool:
+ """Return firmware version."""
+ if self._energysite.resource_type == RESOURCE_TYPE_BATTERY:
+ return self._energysite.version
+ # Non-Powerwall sites do not provide version info
+ return "Unavailable"
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device info."""
+ model = f"{self._energysite.resource_type.title()} {self._energysite.solar_type.replace('_', ' ')}"
+ return DeviceInfo(
+ identifiers={(DOMAIN, self._energysite.energysite_id)},
+ manufacturer="Tesla",
+ model=model,
+ name=self._energysite.site_name,
+ sw_version=self.sw_version,
+ )
diff --git a/custom_components/tesla_custom/binary_sensor.py b/custom_components/tesla_custom/binary_sensor.py
index 1c439db0..dbcdc8aa 100644
--- a/custom_components/tesla_custom/binary_sensor.py
+++ b/custom_components/tesla_custom/binary_sensor.py
@@ -1,40 +1,184 @@
-"""Support for Tesla binary sensor."""
+"""Support for Tesla binary sensors."""
+import logging
-from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.const import GRID_ACTIVE, RESOURCE_TYPE_BATTERY
+from teslajsonpy.energy import PowerwallSite
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
+from homeassistant.components.binary_sensor import (
+ BinarySensorDeviceClass,
+ BinarySensorEntity,
+)
+from homeassistant.core import HomeAssistant
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity, TeslaEnergyEntity
+from .const import DOMAIN
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- async_add_entities(
- [
- TeslaBinarySensor(
- device,
- hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"],
- )
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
- "binary_sensor"
- ]
- ],
- True,
- )
+_LOGGER = logging.getLogger(__name__)
-class TeslaBinarySensor(TeslaDevice, BinarySensorEntity):
- """Implement an Tesla binary sensor for parking and charger."""
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla selects by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
+ entities = []
+
+ for car in cars.values():
+ entities.append(TeslaCarParkingBrake(hass, car, coordinator))
+ entities.append(TeslaCarOnline(hass, car, coordinator))
+ entities.append(TeslaCarChargerConnection(hass, car, coordinator))
+ entities.append(TeslaCarCharging(hass, car, coordinator))
+
+ for energysite in energysites.values():
+ if energysite.resource_type == RESOURCE_TYPE_BATTERY:
+ entities.append(TeslaEnergyBatteryCharging(hass, energysite, coordinator))
+ entities.append(TeslaEnergyGridStatus(hass, energysite, coordinator))
+
+ async_add_entities(entities, True)
+
+
+class TeslaCarParkingBrake(TeslaCarEntity, BinarySensorEntity):
+ """Representation of a Tesla car parking brake binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize parking brake entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "parking brake"
+ self._attr_icon = "mdi:car-brake-parking"
+ self._attr_device_class = None
+
+ @property
+ def is_on(self):
+ """Return True if car shift state in park or None."""
+ # When car is parked and off, Tesla API reports shift_state None
+ return self._car.shift_state == "P" or self._car.shift_state is None
+
+
+class TeslaCarChargerConnection(TeslaCarEntity, BinarySensorEntity):
+ """Representation of a Tesla car charger connection binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charger connection entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charger"
+ self._attr_icon = "mdi:ev-station"
+ self._attr_device_class = BinarySensorDeviceClass.PLUG
@property
- def device_class(self):
- """Return the class of this binary sensor."""
- return (
- self.tesla_device.sensor_type
- if self.tesla_device.sensor_type in DEVICE_CLASSES
- else None
- )
+ def is_on(self):
+ """Return True if charger connected."""
+ return self._car.charging_state != "Disconnected"
+
+ @property
+ def extra_state_attributes(self):
+ """Return device state attributes."""
+ return {
+ "charging_state": self._car.charging_state,
+ "conn_charge_cable": self._car.conn_charge_cable,
+ "fast_charger_present": self._car.fast_charger_present,
+ "fast_charger_brand": self._car.fast_charger_brand,
+ "fast_charger_type": self._car.fast_charger_type,
+ }
+
+
+class TeslaCarCharging(TeslaCarEntity, BinarySensorEntity):
+ """Representation of Tesla car charging binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charging entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charging"
+ self._attr_icon = "mdi:ev-station"
+ self._attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
@property
def is_on(self):
"""Return the state of the binary sensor."""
- return self.tesla_device.get_value()
+ return self._car.charging_state == "Charging"
+
+
+class TeslaCarOnline(TeslaCarEntity, BinarySensorEntity):
+ """Representation of a Tesla car online binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize car online entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "online"
+ self._attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
+
+ @property
+ def is_on(self):
+ """Return True if car is online."""
+ return self._car.is_on
+
+ @property
+ def extra_state_attributes(self):
+ """Return device state attributes."""
+ return {
+ "vehicle_id": str(self._car.vehicle_id),
+ "vin": self._car.vin,
+ "id": str(self._car.id),
+ }
+
+
+class TeslaEnergyBatteryCharging(TeslaEnergyEntity, BinarySensorEntity):
+ """Representation of a Tesla energy charging binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize battery charging entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "battery charging"
+ self._attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
+ self._attr_icon = "mdi:battery-charging"
+
+ @property
+ def is_on(self) -> bool:
+ """Return True if battery charging."""
+ return self._energysite.battery_power < -100
+
+
+class TeslaEnergyGridStatus(TeslaEnergyEntity, BinarySensorEntity):
+ """Representation of the Tesla energy grid status binary sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize grid status entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "grid status"
+ self._attr_device_class = BinarySensorDeviceClass.POWER
+
+ @property
+ def is_on(self) -> bool:
+ """Return True if grid status is active."""
+ return self._energysite.grid_status == GRID_ACTIVE
diff --git a/custom_components/tesla_custom/button.py b/custom_components/tesla_custom/button.py
index 4c4d3163..a0141637 100644
--- a/custom_components/tesla_custom/button.py
+++ b/custom_components/tesla_custom/button.py
@@ -1,86 +1,138 @@
-"""Support for Tesla charger buttons."""
-from custom_components.tesla_custom.const import ICONS
+"""Support for Tesla buttons."""
import logging
-_LOGGER = logging.getLogger(__name__)
+from teslajsonpy.car import TeslaCar
from homeassistant.components.button import ButtonEntity
-from teslajsonpy.exceptions import HomelinkError
-
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla button by config_entry."""
- coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"]
- entities = []
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["button"]:
- if device.type == "horn":
- entities.append(Horn(device, coordinator))
- elif device.type == "flash lights":
- entities.append(FlashLights(device, coordinator))
- elif device.type == "trigger homelink":
- entities.append(TriggerHomelink(device, coordinator))
- async_add_entities(entities, True)
+_LOGGER = logging.getLogger(__name__)
-class Horn(TeslaDevice, ButtonEntity):
- """Representation of a Tesla horn button."""
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla selects by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ entities = []
- def __init__(self, tesla_device, coordinator):
- """Initialise the button."""
- super().__init__(tesla_device, coordinator)
- self.controller = coordinator.controller
+ for car in cars.values():
+ entities.append(TeslaCarHorn(hass, car, coordinator))
+ entities.append(TeslaCarFlashLights(hass, car, coordinator))
+ entities.append(TeslaCarWakeUp(hass, car, coordinator))
+ entities.append(TeslaCarForceDataUpdate(hass, car, coordinator))
+ if car.homelink_device_count:
+ entities.append(TeslaCarTriggerHomelink(hass, car, coordinator))
- @TeslaDevice.Decorators.check_for_reauth
- async def async_press(self, **kwargs):
- """Send the command."""
- _LOGGER.debug("Honk horn: %s", self.name)
- await self.tesla_device.honk_horn()
+ async_add_entities(entities, True)
-class FlashLights(TeslaDevice, ButtonEntity):
- """Representation of a Tesla flash lights button."""
+class TeslaCarHorn(TeslaCarEntity, ButtonEntity):
+ """Representation of a Tesla car horn button."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize horn entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "horn"
+ self._attr_icon = "mdi:bullhorn"
+
+ async def async_press(self) -> None:
+ """Handle the button press."""
+ await self._car.honk_horn()
+
+
+class TeslaCarFlashLights(TeslaCarEntity, ButtonEntity):
+ """Representation of a Tesla car flash lights button."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize flash light entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "flash lights"
+ self._attr_icon = "mdi:car-light-high"
+
+ async def async_press(self) -> None:
+ """Handle the button press."""
+ await self._car.flash_lights()
+
+
+class TeslaCarWakeUp(TeslaCarEntity, ButtonEntity):
+ """Representation of a Tesla car wake up button"""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize wake up button."""
+ super().__init__(hass, car, coordinator)
+ self.type = "wake up"
+ self._attr_icon = "mdi:moon-waning-crescent"
+ self._attr_entity_category = EntityCategory.DIAGNOSTIC
+
+ async def async_press(self) -> None:
+ """Handle the button press."""
+ await self._car.wake_up()
- def __init__(self, tesla_device, coordinator):
- """Initialise the button."""
- super().__init__(tesla_device, coordinator)
- self.controller = coordinator.controller
+ @property
+ def available(self) -> bool:
+ """Return True."""
+ return True
- @TeslaDevice.Decorators.check_for_reauth
- async def async_press(self, **kwargs):
- """Send the command."""
- _LOGGER.debug("Flash lights: %s", self.name)
- await self.tesla_device.flash_lights()
+class TeslaCarForceDataUpdate(TeslaCarEntity, ButtonEntity):
+ """Representation of a Tesla car force data update button."""
-class TriggerHomelink(TeslaDevice, ButtonEntity):
- """Representation of a Tesla Homelink button."""
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize force data update button."""
+ super().__init__(hass, car, coordinator)
+ self.type = "force data update"
+ self._attr_icon = "mdi:database-sync"
+ self._attr_entity_category = EntityCategory.DIAGNOSTIC
- def __init__(self, tesla_device, coordinator):
- """Initialise the button."""
- super().__init__(tesla_device, coordinator)
- self.controller = coordinator.controller
- self.__waiting = False
+ async def async_press(self) -> None:
+ """Handle the button press."""
+ await self.update_controller(wake_if_asleep=True, force=True)
@property
def available(self) -> bool:
- """Return True if entity is available."""
- return (
- super().available and self.tesla_device.available() and not self.__waiting
- )
+ """Return True."""
+ return True
+
+
+class TeslaCarTriggerHomelink(TeslaCarEntity, ButtonEntity):
+ """Representation of a Tesla car Homelink button."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialise Homelink button."""
+ super().__init__(hass, car, coordinator)
+ self.type = "homelink"
+ self._attr_icon = "mdi:garage"
- @TeslaDevice.Decorators.check_for_reauth
- async def async_press(self, **kwargs):
+ async def async_press(self):
"""Send the command."""
- _LOGGER.debug("Trigger homelink: %s", self.name)
- self.__waiting = True
- self.async_write_ha_state()
- try:
- await self.tesla_device.trigger_homelink()
- except HomelinkError as ex:
- _LOGGER.error("%s", ex.message)
- finally:
- self.__waiting = False
- self.async_write_ha_state()
+ await self._car.trigger_homelink()
diff --git a/custom_components/tesla_custom/climate.py b/custom_components/tesla_custom/climate.py
index b1eea7f1..cbf5b840 100644
--- a/custom_components/tesla_custom/climate.py
+++ b/custom_components/tesla_custom/climate.py
@@ -1,63 +1,66 @@
-"""Support for Tesla HVAC system."""
-from __future__ import annotations
-
+"""Support for Tesla climate."""
import logging
+from teslajsonpy.car import TeslaCar
+
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
+ DEFAULT_MAX_TEMP,
+ DEFAULT_MIN_TEMP,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
-from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
-from homeassistant.util import slugify
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
-from teslajsonpy.exceptions import UnknownPresetMode
-
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
-from .helpers import get_device, enable_entity
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SUPPORT_HVAC = [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF]
+SUPPORT_PRESET = ["Normal", "Defrost", "Keep On", "Dog Mode", "Camp Mode"]
+
+KEEPER_MAP = {
+ "Keep On": 1,
+ "Dog Mode": 2,
+ "Camp Mode": 3,
+}
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry, async_add_entities
+) -> None:
+ """Set up the Tesla climate by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+
+ entities = [
+ TeslaCarClimate(
+ hass,
+ car,
+ coordinator,
+ )
+ for car in cars.values()
+ ]
+ async_add_entities(entities, True)
-CLIMATE_DEVICES = [
- ["switch", "heated steering switch", "steering_wheel_heater"],
- ["select", "heated seat left", "seat_heater_left"],
- ["select", "heated seat right", "seat_heater_right"],
- ["select", "heated seat rear_left", "seat_heater_rear_left"],
- ["select", "heated seat rear_center", "seat_heater_rear_center"],
- ["select", "heated seat rear_right", "seat_heater_rear_right"],
- ["select", "heated seat third_row_left", "seat_heater_third_row_left"],
- ["select", "heated seat third_row_right", "seat_heater_third_row_right"],
-]
-
-
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- async_add_entities(
- [
- TeslaThermostat(
- device,
- hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"],
- )
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
- "climate"
- ]
- ],
- True,
- )
-
-
-class TeslaThermostat(TeslaDevice, ClimateEntity):
- """Representation of a Tesla climate."""
-
- def __init__(self, tesla_device, coordinator):
- """Initialize of the sensor."""
- super().__init__(tesla_device, coordinator)
- self._entities_enabled = False
+
+class TeslaCarClimate(TeslaCarEntity, ClimateEntity):
+ """Representation of a Tesla car climate."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize climate entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "HVAC (climate) system"
@property
def supported_features(self):
@@ -70,13 +73,14 @@ def hvac_mode(self):
Need to be one of HVAC_MODE_*.
"""
- if self.tesla_device.is_hvac_enabled():
+ if self._car.is_climate_on:
return HVAC_MODE_HEAT_COOL
+
return HVAC_MODE_OFF
@property
def hvac_modes(self):
- """Return the list of available hvac operation modes.
+ """Return list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
@@ -84,143 +88,99 @@ def hvac_modes(self):
@property
def temperature_unit(self):
- """Return the unit of measurement."""
- if self.tesla_device.measurement == "F":
- return TEMP_FAHRENHEIT
+ """Return unit of measurement.
+
+ Tesla API always returns in Celsius.
+ """
return TEMP_CELSIUS
@property
def current_temperature(self):
- """Return the current temperature."""
- return self.tesla_device.get_current_temp()
+ """Return current temperature."""
+ return self._car.inside_temp
+
+ @property
+ def max_temp(self):
+ """Return max temperature."""
+ if self._car.max_avail_temp:
+ return self._car.max_avail_temp
+
+ return DEFAULT_MAX_TEMP
+
+ @property
+ def min_temp(self):
+ """Return min temperature"""
+ if self._car.min_avail_temp:
+ return self._car.min_avail_temp
+
+ return DEFAULT_MIN_TEMP
@property
def target_temperature(self):
- """Return the temperature we try to reach."""
- return self.tesla_device.get_goal_temp()
+ """Return target temperature."""
+ return self._car.driver_temp_setting
- @TeslaDevice.Decorators.check_for_reauth
async def async_set_temperature(self, **kwargs):
- """Set new target temperatures."""
+ """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature:
_LOGGER.debug("%s: Setting temperature to %s", self.name, temperature)
- await self.tesla_device.set_temperature(temperature)
- self.async_write_ha_state()
+ temp = round(temperature, 1)
+
+ await self._car.set_temperature(temp)
+ await self.async_update_ha_state()
- @TeslaDevice.Decorators.check_for_reauth
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
_LOGGER.debug("%s: Setting hvac mode to %s", self.name, hvac_mode)
if hvac_mode == HVAC_MODE_OFF:
- await self.tesla_device.set_status(False)
+ await self._car.set_hvac_mode("off")
elif hvac_mode == HVAC_MODE_HEAT_COOL:
- await self.tesla_device.set_status(True)
- await self.update_climate_related_devices()
- self.async_write_ha_state()
-
- @TeslaDevice.Decorators.check_for_reauth
- async def async_set_preset_mode(self, preset_mode: str) -> None:
- """Set new preset mode."""
- _LOGGER.debug("%s: Setting preset_mode to: %s", self.name, preset_mode)
- try:
- await self.tesla_device.set_preset_mode(preset_mode)
- self.async_write_ha_state()
- except UnknownPresetMode as ex:
- _LOGGER.error("%s", ex.message)
+ await self._car.set_hvac_mode("on")
+ # set_hvac_mode changes multiple states so refresh all entities
+ await self._coordinator.async_refresh()
@property
- def preset_mode(self) -> str | None:
+ def preset_mode(self):
"""Return the current preset mode, e.g., home, away, temp.
Requires SUPPORT_PRESET_MODE.
"""
- return self.tesla_device.preset_mode
+ if self._car.defrost_mode == 2:
+ return "Defrost"
+ if self._car.climate_keeper_mode == "dog":
+ return "Dog Mode"
+ if self._car.climate_keeper_mode == "camp":
+ return "Camp Mode"
+ if self._car.climate_keeper_mode == "on":
+ return "Keep On"
+
+ return "Normal"
@property
- def preset_modes(self) -> list[str] | None:
+ def preset_modes(self):
"""Return a list of available preset modes.
Requires SUPPORT_PRESET_MODE.
"""
- return self.tesla_device.preset_modes
-
- def refresh(self) -> None:
- """Refresh data."""
-
- super().refresh()
-
- if self._entities_enabled:
- # Already enabled all required entities before.
- return
-
- vin = self.tesla_device.vin()
-
- # Get all the climate parameters so we can determine what is supported by this vin.
- # pylint: disable=protected-access
- climate_params = self.tesla_device._controller.get_climate_params(vin=vin)
- if climate_params is None or len(climate_params) == 0:
- # No data available
- _LOGGER.debug("No data available for vin %s", vin)
- return
-
- # Loop through the climate devices
- for c_device in CLIMATE_DEVICES:
- if climate_params.get(c_device[2], None) is None:
- # This climate device is not available.
- _LOGGER.debug(
- "Device %s (%s) not available for vin %s",
- c_device[1],
- c_device[2],
- vin,
- )
- continue
-
- # Determine unique id for this entity.
- unique_id = slugify(
- f"Tesla Model {str(vin[3]).upper()} {vin[-6:]} {c_device[1]}"
- )
- enable_entity(self.hass, c_device[0], TESLA_DOMAIN, unique_id)
- self._entities_enabled = True
-
- async def update_climate_related_devices(self):
- """Reset the Manual Update time on climate related devices.
-
- This way, their states are correctly reflected if they are dependant on the Climate state.
- """
+ return SUPPORT_PRESET
- # This is really gross, and i kinda hate it.
- # but its the only way i could figure out how to force an update on the underlying device
- # thats in the teslajsonpy library.
- # This could be fixed by doing a pr in the underlying library,
- # but is ok for now.
-
- # This works by reseting the last update time in the underlying device.
- # this does not cause an api call, but instead enabled the undering device
- # to read from the shared climate data cache in the teslajsonpy library.
-
- # First, we need to force the controller to update, as the refresh functions asume it
- # has been uddated.
- # We have to manually update the controller becuase changing the HVAC state only updates its state in Home assistant,
- # and not the underlying cache in cliamte_parms. This does mean we talk to Tesla, but we only do so Once.
-
- # pylint: disable=protected-access
- await self.tesla_device._controller.update(
- self.tesla_device._id, wake_if_asleep=False, force=True
- )
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set new preset mode."""
+ _LOGGER.debug("%s: Setting preset_mode to: %s", self.name, preset_mode)
- vin = self.tesla_device.vin()
+ if preset_mode == "Normal":
+ # If setting Normal, we need to check Defrost And Keep modes.
+ if self._car.defrost_mode != 0:
+ await self._car.set_max_defrost(0)
- for c_device in CLIMATE_DEVICES:
- _LOGGER.debug("Refreshing Device: %s.%s", c_device[0], c_device[1])
+ if self._car.climate_keeper_mode != 0:
+ await self._car.set_climate_keeper_mode(0)
- device = await get_device(
- self.hass, self.config_entry_id, c_device[0], c_device[1], vin
- )
- if device is not None:
- class_name = device.__class__.__name__
- attr_str = f"_{class_name}__manual_update_time"
- setattr(device, attr_str, 0)
+ elif preset_mode == "Defrost":
+ await self._car.set_max_defrost(2)
- # Does not cause an API call.
- device.refresh()
+ else:
+ await self._car.set_climate_keeper_mode(KEEPER_MAP[preset_mode])
+ # max_defrost changes multiple states so refresh all entities
+ await self._coordinator.async_refresh()
diff --git a/custom_components/tesla_custom/config_flow.py b/custom_components/tesla_custom/config_flow.py
index 4d64806f..9219e55b 100644
--- a/custom_components/tesla_custom/config_flow.py
+++ b/custom_components/tesla_custom/config_flow.py
@@ -24,6 +24,8 @@
ATTR_POLLING_POLICY_CONNECTED,
ATTR_POLLING_POLICY_NORMAL,
CONF_EXPIRATION,
+ CONF_INCLUDE_VEHICLES,
+ CONF_INCLUDE_ENERGYSITES,
CONF_POLLING_POLICY,
CONF_WAKE_ON_START,
DEFAULT_POLLING_POLICY,
@@ -61,6 +63,8 @@ async def async_step_user(self, user_input=None):
try:
info = await validate_input(self.hass, user_input)
+ # Used for only forcing cars awake on initial setup in async_setup_entry
+ info.update({"initial_setup": True})
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -105,6 +109,8 @@ def _async_schema(self):
vol.Required(CONF_USERNAME, default=self.username): str,
vol.Required(CONF_TOKEN): str,
vol.Required(CONF_DOMAIN, default=AUTH_DOMAIN): str,
+ vol.Required(CONF_INCLUDE_VEHICLES, default=True): bool,
+ vol.Required(CONF_INCLUDE_ENERGYSITES, default=True): bool,
}
)
@@ -160,7 +166,7 @@ async def async_step_init(self, user_input=None):
return self.async_show_form(step_id="init", data_schema=data_schema)
-async def validate_input(hass: core.HomeAssistant, data):
+async def validate_input(hass: core.HomeAssistant, data) -> dict:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -185,6 +191,8 @@ async def validate_input(hass: core.HomeAssistant, data):
config[CONF_EXPIRATION] = result[CONF_EXPIRATION]
config[CONF_USERNAME] = data[CONF_USERNAME]
config[CONF_DOMAIN] = data.get(CONF_DOMAIN, AUTH_DOMAIN)
+ config[CONF_INCLUDE_VEHICLES] = data[CONF_INCLUDE_VEHICLES]
+ config[CONF_INCLUDE_ENERGYSITES] = data[CONF_INCLUDE_ENERGYSITES]
except IncompleteCredentials as ex:
_LOGGER.error("Authentication error: %s %s", ex.message, ex)
diff --git a/custom_components/tesla_custom/const.py b/custom_components/tesla_custom/const.py
index db2c226f..e1831801 100644
--- a/custom_components/tesla_custom/const.py
+++ b/custom_components/tesla_custom/const.py
@@ -1,9 +1,12 @@
"""Const file for Tesla cars."""
-VERSION = "2.4.4"
-CONF_WAKE_ON_START = "enable_wake_on_start"
+VERSION = "2.1.1"
CONF_EXPIRATION = "expiration"
+CONF_INCLUDE_VEHICLES = "include_vehicles"
+CONF_INCLUDE_ENERGYSITES = "include_energysites"
CONF_POLLING_POLICY = "polling_policy"
+CONF_WAKE_ON_START = "enable_wake_on_start"
DOMAIN = "tesla_custom"
+ATTRIBUTION = "Data provided by Tesla"
DATA_LISTENER = "listener"
DEFAULT_SCAN_INTERVAL = 660
DEFAULT_WAKE_ON_START = False
@@ -14,34 +17,19 @@
"sensor",
"lock",
"climate",
+ "cover",
"binary_sensor",
"device_tracker",
"switch",
"button",
"select",
+ "update",
+ "number",
]
-ICONS = {
- "battery sensor": "mdi:battery",
- "range sensor": "mdi:gauge",
- "mileage sensor": "mdi:counter",
- "parking brake sensor": "mdi:car-brake-parking",
- "charger sensor": "mdi:ev-station",
- "charger switch": "mdi:battery-charging",
- "update switch": "mdi:car-connected",
- "maxrange switch": "mdi:gauge-full",
- "temperature sensor": "mdi:thermometer",
- "location tracker": "mdi:crosshairs-gps",
- "charging rate sensor": "mdi:speedometer",
- "sentry mode switch": "mdi:shield-car",
- "horn": "mdi:bullhorn",
- "flash lights": "mdi:car-light-high",
- "trigger homelink": "mdi:garage",
- "solar panel": "mdi:solar-panel",
- "heated steering wheel": "mdi:steering",
-}
AUTH_CALLBACK_PATH = "/auth/tesla/callback"
AUTH_CALLBACK_NAME = "auth:tesla:callback"
+AUTH_DOMAIN_CHINA = "https://auth.tesla.cn"
AUTH_PROXY_PATH = "/auth/tesla/proxy"
AUTH_PROXY_NAME = "auth:tesla:proxy"
diff --git a/custom_components/tesla_custom/cover.py b/custom_components/tesla_custom/cover.py
new file mode 100644
index 00000000..87d67690
--- /dev/null
+++ b/custom_components/tesla_custom/cover.py
@@ -0,0 +1,136 @@
+"""Support for Tesla covers."""
+import logging
+
+from teslajsonpy.car import TeslaCar
+
+from homeassistant.components.cover import (
+ CoverEntity,
+ CoverDeviceClass,
+ CoverEntityFeature,
+)
+from homeassistant.core import HomeAssistant
+
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla locks by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ entities = []
+
+ for car in cars.values():
+ entities.append(TeslaCarChargerDoor(hass, car, coordinator))
+ entities.append(TeslaCarFrunk(hass, car, coordinator))
+ entities.append(TeslaCarTrunk(hass, car, coordinator))
+
+ async_add_entities(entities, True)
+
+
+class TeslaCarChargerDoor(TeslaCarEntity, CoverEntity):
+ """Representation of a Tesla car charger door cover."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charger door cover entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charger door"
+ self._attr_device_class = CoverDeviceClass.DOOR
+ self._attr_icon = "mdi:ev-plug-tesla"
+ self._attr_supported_features = (
+ CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
+ )
+
+ async def async_close_cover(self, **kwargs):
+ """Send close cover command."""
+ _LOGGER.debug("Closing cover: %s", self.name)
+ await self._car.charge_port_door_close()
+ await self.async_update_ha_state()
+
+ async def async_open_cover(self, **kwargs):
+ """Send open cover command."""
+ _LOGGER.debug("Opening cover: %s", self.name)
+ await self._car.charge_port_door_open()
+ await self.async_update_ha_state()
+
+ @property
+ def is_closed(self):
+ """Return True if charger door is closed."""
+ return not self._car.is_charge_port_door_open
+
+
+class TeslaCarFrunk(TeslaCarEntity, CoverEntity):
+ """Representation of a Tesla car frunk lock."""
+
+ def __init__(
+ self, hass: HomeAssistant, car: dict, coordinator: TeslaDataUpdateCoordinator
+ ) -> None:
+ """Initialize frunk lock entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "frunk"
+ self._attr_device_class = CoverDeviceClass.DOOR
+ self._attr_icon = "mdi:car"
+ self._attr_supported_features = CoverEntityFeature.OPEN
+
+ async def async_open_cover(self, **kwargs):
+ """Send open cover command."""
+ _LOGGER.debug("Opening cover: %s", self.name)
+ if self.is_closed is True:
+ await self._car.toggle_frunk()
+ await self.async_update_ha_state()
+
+ @property
+ def is_closed(self):
+ """Return True if frunk is closed."""
+ return self._car.is_frunk_closed
+
+
+class TeslaCarTrunk(TeslaCarEntity, CoverEntity):
+ """Representation of a Tesla car trunk cover."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize trunk cover entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "trunk"
+ self._attr_device_class = CoverDeviceClass.DOOR
+ self._attr_icon = "mdi:car-back"
+
+ async def async_close_cover(self, **kwargs):
+ """Send close cover command."""
+ _LOGGER.debug("Closing cover: %s", self.name)
+ if self.is_closed is False:
+ await self._car.toggle_trunk()
+ await self.async_update_ha_state()
+
+ async def async_open_cover(self, **kwargs):
+ """Send open cover command."""
+ _LOGGER.debug("Opening cover: %s", self.name)
+ if self.is_closed is True:
+ await self._car.toggle_trunk()
+ await self.async_update_ha_state()
+
+ @property
+ def is_closed(self):
+ """Return True if trunk is closed."""
+ return self._car.is_trunk_closed
+
+ @property
+ def supported_features(self) -> int:
+ """Return supported features."""
+ if self._car.powered_lift_gate:
+ return CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
+
+ return CoverEntityFeature.OPEN
diff --git a/custom_components/tesla_custom/device_tracker.py b/custom_components/tesla_custom/device_tracker.py
index b6102a26..a7c2d3e4 100644
--- a/custom_components/tesla_custom/device_tracker.py
+++ b/custom_components/tesla_custom/device_tracker.py
@@ -1,92 +1,127 @@
-"""Support for tracking Tesla cars."""
-from __future__ import annotations
+"""Support for Tesla device tracker."""
+import logging
+import math
+
+from teslajsonpy.car import TeslaCar
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity
+from homeassistant.core import HomeAssistant
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import AUTH_DOMAIN_CHINA, DOMAIN
-import math
+_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- entities = [
- TeslaDeviceEntity(
- device,
- hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"],
- )
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
- "devices_tracker"
- ]
- ]
- async_add_entities(entities, True)
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla device trackers by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ entities = []
+ for car in cars.values():
+ entities.append(TeslaCarLocation(hass, car, coordinator))
-class TeslaDeviceEntity(TeslaDevice, TrackerEntity):
- """A class representing a Tesla device."""
+ async_add_entities(entities, True)
- def __init__(self, tesla_device, coordinator):
- super().__init__(tesla_device, coordinator)
- if str(tesla_device._controller._Controller__connection.auth_domain) == "https://auth.tesla.cn":
- self.in_china = True
+
+class TeslaCarLocation(TeslaCarEntity, TrackerEntity):
+ """Representation of a Tesla car location device tracker."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize car location entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "location tracker"
+ if (
+ str(coordinator.controller._Controller__connection.auth_domain)
+ == AUTH_DOMAIN_CHINA
+ ):
+ self._in_china = True
else:
- self.in_china = False
- self.location_converter = LocationConverter()
+ self._in_china = False
+ self._location_converter = LocationConverter()
@property
- def force_update(self):
- """Disable forced updated since we are polling via the coordinator updates."""
- return False
+ def source_type(self):
+ """Return device tracker source type."""
+ return SOURCE_TYPE_GPS
@property
- def latitude(self) -> float | None:
- """Return latitude value of the device."""
- location = self.tesla_device.get_location()
- if location and self.in_china:
- location = self.location_converter.gcj02towgs84(location)
- return location.get("latitude") if location else None
+ def longitude(self):
+ """Return longitude."""
+ location = self.location
- @property
- def longitude(self) -> float | None:
- """Return longitude value of the device."""
- location = self.tesla_device.get_location()
- if location and self.in_china:
- location = self.location_converter.gcj02towgs84(location)
- return location.get("longitude") if location else None
+ if self._in_china:
+ location = self._location_converter.gcj02towgs84(location)
+ return location.get("longitude)")
+
+ return location.get("longitude")
@property
- def source_type(self):
- """Return the source type, eg gps or router, of the device."""
- return SOURCE_TYPE_GPS
+ def latitude(self):
+ """Return latitude."""
+ location = self.location
+
+ if self._in_china:
+ location = self._location_converter.gcj02towgs84(location)
+ return location.get("latitude)")
+
+ return location.get("latitude")
@property
def extra_state_attributes(self):
- """Return the state attributes of the device."""
- attr = super().extra_state_attributes.copy()
- location = self.tesla_device.get_location()
- if location:
- attr.update(
- {
- "trackr_id": self.unique_id,
- "heading": location["heading"],
- "speed": location["speed"],
- }
- )
- return attr
+ """Return device state attributes."""
+ # "native_heading" does not exist in Tesla API with 2015 Model S 85D - newer models only?
+ # if self._car.native_location_supported:
+ # heading = self._car.native_heading
+ # else:
+ # heading = self._car.heading
+
+ return {
+ "heading": self._car.heading,
+ "speed": self._car.speed,
+ }
+
+ @property
+ def force_update(self):
+ """Disable forced updated since we are polling via the coordinator updates."""
+ return False
+
+ @property
+ def location(self) -> dict:
+ """Return car location as a dictionary."""
+ if self._car.native_location_supported:
+ return {
+ "longitude": self._car.native_longitude,
+ "latitude": self._car.native_latitude,
+ }
+
+ return {
+ "longitude": self._car.longitude,
+ "latitude": self._car.latitude,
+ }
class LocationConverter:
- """Convert gcj02 to wgs84 for Chinese user"""
+ # pylint: disable=invalid-name
+ """Convert gcj02 to wgs84 for Chinese users."""
- def __init__(self):
+ def __init__(self) -> None:
+ """Initialize LocationConverter."""
self.x_pi = 3.14159265358979324 * 3000.0 / 180.0
self.pi = 3.1415926535897932384626
self.a = 6378245.0
self.ee = 0.00669342162296594323
- def gcj02towgs84(self, location):
+ def gcj02towgs84(self, location) -> dict:
+ """Convert gcj02 to wgs84."""
lng = location.get("longitude")
lat = location.get("latitude")
dlat = self.transform_lat(lng - 105.0, lat - 35.0)
@@ -95,24 +130,79 @@ def gcj02towgs84(self, location):
magic = math.sin(radlat)
magic = 1 - self.ee * magic * magic
sqrtmagic = math.sqrt(magic)
- dlat = (dlat * 180.0) / ((self.a * (1 - self.ee)) / (magic * sqrtmagic) * self.pi)
+ dlat = (dlat * 180.0) / (
+ (self.a * (1 - self.ee)) / (magic * sqrtmagic) * self.pi
+ )
dlng = (dlng * 180.0) / (self.a / sqrtmagic * math.cos(radlat) * self.pi)
mglat = lat + dlat
mglng = lng + dlng
location["longitude"] = lng * 2 - mglng
location["latitude"] = lat * 2 - mglat
+
return location
def transform_lng(self, lng, lat) -> float:
- ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
- ret += (20.0 * math.sin(6.0 * lng * self.pi) + 20.0 * math.sin(2.0 * lng * self.pi)) * 2.0 / 3.0
- ret += (20.0 * math.sin(lng * self.pi) + 40.0 * math.sin(lng / 3.0 * self.pi)) * 2.0 / 3.0
- ret += (150.0 * math.sin(lng / 12.0 * self.pi) + 300.0 * math.sin(lng / 30.0 * self.pi)) * 2.0 / 3.0
+ """Transform longitude."""
+ ret = (
+ 300.0
+ + lng
+ + 2.0 * lat
+ + 0.1 * lng * lng
+ + 0.1 * lng * lat
+ + 0.1 * math.sqrt(math.fabs(lng))
+ )
+ ret += (
+ (
+ 20.0 * math.sin(6.0 * lng * self.pi)
+ + 20.0 * math.sin(2.0 * lng * self.pi)
+ )
+ * 2.0
+ / 3.0
+ )
+ ret += (
+ (20.0 * math.sin(lng * self.pi) + 40.0 * math.sin(lng / 3.0 * self.pi))
+ * 2.0
+ / 3.0
+ )
+ ret += (
+ (
+ 150.0 * math.sin(lng / 12.0 * self.pi)
+ + 300.0 * math.sin(lng / 30.0 * self.pi)
+ )
+ * 2.0
+ / 3.0
+ )
return ret
def transform_lat(self, lng, lat) -> float:
- ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
- ret += (20.0 * math.sin(6.0 * lng * self.pi) + 20.0 * math.sin(2.0 * lng * self.pi)) * 2.0 / 3.0
- ret += (20.0 * math.sin(lat * self.pi) + 40.0 * math.sin(lat / 3.0 * self.pi)) * 2.0 / 3.0
- ret += (160.0 * math.sin(lat / 12.0 * self.pi) + 320 * math.sin(lat * self.pi / 30.0)) * 2.0 / 3.0
+ """Transform latitude."""
+ ret = (
+ -100.0
+ + 2.0 * lng
+ + 3.0 * lat
+ + 0.2 * lat * lat
+ + 0.1 * lng * lat
+ + 0.2 * math.sqrt(math.fabs(lng))
+ )
+ ret += (
+ (
+ 20.0 * math.sin(6.0 * lng * self.pi)
+ + 20.0 * math.sin(2.0 * lng * self.pi)
+ )
+ * 2.0
+ / 3.0
+ )
+ ret += (
+ (20.0 * math.sin(lat * self.pi) + 40.0 * math.sin(lat / 3.0 * self.pi))
+ * 2.0
+ / 3.0
+ )
+ ret += (
+ (
+ 160.0 * math.sin(lat / 12.0 * self.pi)
+ + 320 * math.sin(lat * self.pi / 30.0)
+ )
+ * 2.0
+ / 3.0
+ )
return ret
diff --git a/custom_components/tesla_custom/helpers.py b/custom_components/tesla_custom/helpers.py
deleted file mode 100644
index 3b60e77c..00000000
--- a/custom_components/tesla_custom/helpers.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""Helpers module.
-
-A collection of functions which may be used accross entities
-"""
-import logging
-import asyncio
-import async_timeout
-
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_registry import async_get, RegistryEntryDisabler
-
-from .const import DOMAIN as TESLA_DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def get_device(
- hass: HomeAssistant,
- config_entry_id: str,
- device_category: str,
- device_type: str,
- vin: str,
-):
- """Get a tesla Device for a Config Entry ID."""
-
- entry_data = hass.data[TESLA_DOMAIN][config_entry_id]
- devices = entry_data["devices"].get(device_category, [])
-
- for device in devices:
- if device.type == device_type and device.vin() == vin:
- return device
-
- return None
-
-
-def enable_entity(
- hass: HomeAssistant,
- domain: str,
- platform: str,
- unique_id: str,
-):
- """Enable provided entity if disabled by integration."""
-
- # Get Entity Registry
- entity_registry = async_get(hass)
-
- entity_id = entity_registry.async_get_entity_id(domain, platform, unique_id)
- if entity_id is None:
- _LOGGER.debug(
- "Entity for domain %s, platform %s with unique id %s "
- "was never registered.",
- domain,
- platform,
- unique_id,
- )
- return
-
- # Now get the entity itself.
- entity = entity_registry.entities[entity_id]
-
- if entity.disabled_by == RegistryEntryDisabler.INTEGRATION:
- # Entity was disabled by us, now we can enable it.
- _LOGGER.debug("Enabling entity %s", entity)
- entity_registry.async_update_entity(entity.entity_id, disabled_by=None)
- elif entity.disabled_by is None:
- _LOGGER.debug("Entity %s already enabled", entity_id)
- else:
- _LOGGER.debug("Entity %s was disabled by %s", entity_id, entity.disabled_by)
-
-
-async def wait_for_climate(
- hass: HomeAssistant, config_entry_id: str, vin: str, timeout: int = 30
-):
- """Wait for HVac.
-
- Optional Timeout. defaults to 30 seconds
- """
- climate_device = await get_device(
- hass, config_entry_id, "climate", "HVAC (climate) system", vin
- )
-
- if climate_device is None:
- return None
-
- async with async_timeout.timeout(timeout):
- while True:
- hvac_mode = climate_device.is_hvac_enabled()
-
- if hvac_mode is True:
- _LOGGER.debug("HVAC Enabled")
- return True
-
- _LOGGER.info("Enabing Climate to activate Heated Steering Wheel")
-
- # The below is a blocking funtion (it waits for a reponse from the API).
- # So it could eat into our timeout, and this is fine.
- # We'll try to turn the set the status, and check again
- try:
- await climate_device.set_status(True)
- continue
- except:
- # If we get an error, we'll just loop around and try again
- pass
-
- # Wait two second between API calls, in case we get an error like
- # car unavailable,or any other random thing tesla throws at us
- await asyncio.sleep(2)
-
- # we'll return false if the timeout is reached.
- return False
diff --git a/custom_components/tesla_custom/lock.py b/custom_components/tesla_custom/lock.py
index f902117f..e7e15b49 100644
--- a/custom_components/tesla_custom/lock.py
+++ b/custom_components/tesla_custom/lock.py
@@ -1,44 +1,56 @@
-"""Support for Tesla door locks."""
+"""Support for Tesla locks."""
import logging
+from teslajsonpy.car import TeslaCar
+
from homeassistant.components.lock import LockEntity
+from homeassistant.core import HomeAssistant
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- entities = [
- TeslaLock(
- device,
- hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"],
- )
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"]
- ]
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla locks by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ entities = []
+
+ for car in cars.values():
+ entities.append(TeslaCarDoors(hass, car, coordinator))
+
async_add_entities(entities, True)
-class TeslaLock(TeslaDevice, LockEntity):
- """Representation of a Tesla door lock."""
+class TeslaCarDoors(TeslaCarEntity, LockEntity):
+ """Representation of a Tesla car door lock."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize door lock entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "doors"
- @TeslaDevice.Decorators.check_for_reauth
async def async_lock(self, **kwargs):
- """Send the lock command."""
- _LOGGER.debug("Locking doors for: %s", self.name)
- await self.tesla_device.lock()
- self.async_write_ha_state()
+ """Send lock command."""
+ _LOGGER.debug("Locking: %s", self.name)
+ await self._car.lock()
+ await self.async_update_ha_state()
- @TeslaDevice.Decorators.check_for_reauth
async def async_unlock(self, **kwargs):
- """Send the unlock command."""
- _LOGGER.debug("Unlocking doors for: %s", self.name)
- await self.tesla_device.unlock()
- self.async_write_ha_state()
+ """Send unlock command."""
+ _LOGGER.debug("Unlocking: %s", self.name)
+ await self._car.unlock()
+ await self.async_update_ha_state()
@property
def is_locked(self):
- """Get whether the lock is in locked state."""
- return self.tesla_device.is_locked()
+ """Return True if door is locked."""
+ return self._car.is_locked
diff --git a/custom_components/tesla_custom/manifest.json b/custom_components/tesla_custom/manifest.json
index e055448f..084325f0 100644
--- a/custom_components/tesla_custom/manifest.json
+++ b/custom_components/tesla_custom/manifest.json
@@ -4,9 +4,15 @@
"config_flow": true,
"documentation": "https://github.com/alandtse/tesla/wiki",
"issue_tracker": "https://github.com/alandtse/tesla/issues",
- "requirements": ["teslajsonpy==2.4.5"],
- "codeowners": ["@alandtse"],
- "dependencies": ["http"],
+ "requirements": [
+ "teslajsonpy==3.0.0"
+ ],
+ "codeowners": [
+ "@alandtse"
+ ],
+ "dependencies": [
+ "http"
+ ],
"dhcp": [
{
"hostname": "tesla_*",
@@ -23,4 +29,4 @@
],
"iot_class": "cloud_polling",
"version": "2.4.4"
-}
+}
\ No newline at end of file
diff --git a/custom_components/tesla_custom/number.py b/custom_components/tesla_custom/number.py
new file mode 100644
index 00000000..957fb84c
--- /dev/null
+++ b/custom_components/tesla_custom/number.py
@@ -0,0 +1,167 @@
+"""Support for Tesla numbers."""
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.const import (
+ BACKUP_RESERVE_MAX,
+ BACKUP_RESERVE_MIN,
+ CHARGE_CURRENT_MIN,
+ RESOURCE_TYPE_BATTERY,
+)
+from teslajsonpy.energy import PowerwallSite
+
+from homeassistant.components.number import NumberEntity, NumberMode
+from homeassistant.core import HomeAssistant
+from homeassistant.const import ELECTRIC_CURRENT_AMPERE, PERCENTAGE
+from homeassistant.helpers.icon import icon_for_battery_level
+
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity, TeslaEnergyEntity
+from .const import DOMAIN
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla numbers by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
+ entities = []
+
+ for car in cars.values():
+ entities.append(TeslaCarChargeLimit(hass, car, coordinator))
+ entities.append(TeslaCarChargingAmps(hass, car, coordinator))
+
+ for energysite in energysites.values():
+ if energysite.resource_type == RESOURCE_TYPE_BATTERY:
+ entities.append(TeslaEnergyBackupReserve(hass, energysite, coordinator))
+
+ async_add_entities(entities, True)
+
+
+class TeslaCarChargeLimit(TeslaCarEntity, NumberEntity):
+ """Representation of a Tesla car charge limit number."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charge limit entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charge limit"
+ self._attr_icon = "mdi:ev-station"
+ self._attr_mode = NumberMode.AUTO
+ self._attr_native_step = 1
+
+ async def async_set_native_value(self, value: int) -> None:
+ """Update charge limit."""
+ await self._car.change_charge_limit(value)
+ await self.async_update_ha_state()
+
+ @property
+ def native_value(self) -> int:
+ """Return charge limit."""
+ return self._car.charge_limit_soc
+
+ @property
+ def native_min_value(self) -> int:
+ """Return min charge limit."""
+ return self._car.charge_limit_soc_min
+
+ @property
+ def native_max_value(self) -> int:
+ """Return max charge limit."""
+ return self._car.charge_limit_soc_max
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return percentage."""
+ return PERCENTAGE
+
+
+class TeslaCarChargingAmps(TeslaCarEntity, NumberEntity):
+ """Representation of a Tesla car charging amps number."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charging amps entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charging amps"
+ self._attr_icon = "mdi:ev-station"
+ self._attr_mode = NumberMode.AUTO
+ self._attr_native_step = 1
+
+ async def async_set_native_value(self, value: int) -> None:
+ """Update charging amps."""
+ await self._car.set_charging_amps(value)
+ await self.async_update_ha_state()
+
+ @property
+ def native_value(self) -> int:
+ """Return charging amps."""
+ return self._car.charge_current_request
+
+ @property
+ def native_min_value(self) -> int:
+ """Return min charging ampst."""
+ return CHARGE_CURRENT_MIN
+
+ @property
+ def native_max_value(self) -> int:
+ """Return max charging amps."""
+ return self._car.charge_current_request_max
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return percentage."""
+ return ELECTRIC_CURRENT_AMPERE
+
+
+class TeslaEnergyBackupReserve(TeslaEnergyEntity, NumberEntity):
+ """Representation of a Tesla energy backup reserve number."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize backup reserve entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "backup reserve"
+ self._attr_icon = "mdi:battery"
+ self._attr_mode = NumberMode.AUTO
+ self._attr_native_step = 1
+
+ async def async_set_native_value(self, value: int) -> None:
+ """Update backup reserve percentage."""
+ await self._energysite.set_reserve_percent(value)
+ await self.async_update_ha_state()
+
+ @property
+ def native_value(self) -> int:
+ """Return backup reserve percentage."""
+ return self._energysite.backup_reserve_percent
+
+ @property
+ def native_min_value(self) -> int:
+ """Return min backup reserve percentage."""
+ return BACKUP_RESERVE_MIN
+
+ @property
+ def native_max_value(self) -> int:
+ """Return max backup reserve percentage."""
+ return BACKUP_RESERVE_MAX
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return percentage."""
+ return PERCENTAGE
+
+ @property
+ def icon(self):
+ """Return icon for the backup reserve."""
+ return icon_for_battery_level(battery_level=self.native_value)
diff --git a/custom_components/tesla_custom/select.py b/custom_components/tesla_custom/select.py
index 8b91987f..081e8149 100644
--- a/custom_components/tesla_custom/select.py
+++ b/custom_components/tesla_custom/select.py
@@ -1,56 +1,260 @@
"""Support for Tesla selects."""
import logging
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.const import RESOURCE_TYPE_BATTERY
+from teslajsonpy.energy import PowerwallSite, SolarPowerwallSite
+
from homeassistant.components.select import SelectEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
-from .helpers import wait_for_climate
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity, TeslaEnergyEntity
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-OPTIONS = [
+CABIN_OPTIONS = [
+ "Off",
+ "No A/C",
+ "On",
+]
+
+EXPORT_RULE = [
+ "Solar",
+ "Everything",
+]
+
+GRID_CHARGING = [
+ "Yes",
+ "No",
+]
+
+HEATER_OPTIONS = [
"Off",
"Low",
"Medium",
"High",
]
+OPERATION_MODE = [
+ "Self-Powered",
+ "Time-Based Control",
+ "Backup",
+]
+
+SEAT_ID_MAP = {
+ "left": 0,
+ "right": 1,
+ "rear left": 2,
+ "rear center": 4,
+ "rear right": 5,
+ "third row left": 6,
+ "third row right": 7,
+}
-async def async_setup_entry(hass, config_entry, async_add_entities):
+
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla selects by config_entry."""
- coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"]
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
entities = []
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["select"]:
- if device.type.startswith("heated seat "):
- entities.append(HeatedSeatSelect(device, coordinator))
+
+ for car in cars.values():
+ entities.append(TeslaCarCabinOverheatProtection(hass, car, coordinator))
+ for seat_name in SEAT_ID_MAP:
+ if "rear" in seat_name and not car.rear_seat_heaters:
+ continue
+ # Check for str "None" (car does not have third row seats)
+ # or None (car is asleep)
+ if "third" in seat_name and (
+ car.third_row_seats == "None" or car.third_row_seats is None
+ ):
+ continue
+ entities.append(TeslaCarHeatedSeat(hass, car, coordinator, seat_name))
+
+ for energysite in energysites.values():
+ if energysite.resource_type == RESOURCE_TYPE_BATTERY:
+ entities.append(TeslaEnergyOperationMode(hass, energysite, coordinator))
+ if energysite.resource_type == RESOURCE_TYPE_BATTERY and energysite.has_solar:
+ entities.append(TeslaEnergyExportRule(hass, energysite, coordinator))
+ entities.append(TeslaEnergyGridCharging(hass, energysite, coordinator))
+
async_add_entities(entities, True)
-class HeatedSeatSelect(TeslaDevice, SelectEntity):
- """Representation of a Tesla Heated Seat Select."""
+class TeslaCarHeatedSeat(TeslaCarEntity, SelectEntity):
+ """Representation of a Tesla car heated seat select."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ seat_name: str,
+ ):
+ """Initialize heated seat entity."""
+ super().__init__(hass, car, coordinator)
+ self._seat_name = seat_name
+ self.type = f"heated seat {seat_name}"
+ self._attr_icon = "mdi:car-seat-heater"
- @TeslaDevice.Decorators.check_for_reauth
async def async_select_option(self, option: str, **kwargs):
"""Change the selected option."""
- level: int = OPTIONS.index(option)
+ level: int = HEATER_OPTIONS.index(option)
- vin = self.tesla_device.vin()
- await wait_for_climate(self.hass, self.config_entry_id, vin)
_LOGGER.debug("Setting %s to %s", self.name, level)
- await self.tesla_device.set_seat_heat_level(level)
- self.async_write_ha_state()
+ await self._car.remote_seat_heater_request(level, SEAT_ID_MAP[self._seat_name])
+
+ await self.update_controller(force=True)
@property
def current_option(self):
- """Return the selected entity option to represent the entity state."""
- current_value = self.tesla_device.get_seat_heat_level()
+ """Return current heated seat setting."""
+ current_value = self._car.get_seat_heater_status(SEAT_ID_MAP[self._seat_name])
if current_value is None:
- return OPTIONS[0]
- return OPTIONS[current_value]
+ return HEATER_OPTIONS[0]
+ return HEATER_OPTIONS[current_value]
@property
def options(self):
- """Return a set of selectable options."""
- return OPTIONS
+ """Return heated seat options."""
+ return HEATER_OPTIONS
+
+
+class TeslaCarCabinOverheatProtection(TeslaCarEntity, SelectEntity):
+ """Representation of a Tesla car cabin overheat protection select."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ):
+ """Initialize cabin overheat protection entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "cabin overheat protection"
+ self._attr_options = CABIN_OPTIONS
+ self._attr_entity_category = EntityCategory.CONFIG
+ self._attr_icon = "mdi:sun-thermometer"
+
+ async def async_select_option(self, option: str, **kwargs):
+ """Change the selected option."""
+ await self._car.set_cabin_overheat_protection(option)
+ await self.async_update_ha_state()
+
+ @property
+ def current_option(self):
+ """Return current cabin overheat protection setting."""
+ return self._car.cabin_overheat_protection
+
+
+class TeslaEnergyGridCharging(TeslaEnergyEntity, SelectEntity):
+ """Representation of a Tesla energy site grid charging select."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: SolarPowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ):
+ """Initialize grid charging entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "grid charging"
+ self._attr_options = GRID_CHARGING
+
+ async def async_select_option(self, option: str, **kwargs):
+ """Change the selected option."""
+ if option == GRID_CHARGING[0]:
+ await self._energysite.set_grid_charging(True)
+ else:
+ await self._energysite.set_grid_charging(False)
+
+ await self.async_update_ha_state()
+
+ @property
+ def current_option(self):
+ """Return current grid charging setting."""
+ if self._energysite.grid_charging:
+ return GRID_CHARGING[0]
+ return GRID_CHARGING[1]
+
+ @property
+ def icon(self):
+ """Return icon for the grid charging."""
+ if self._energysite.grid_charging:
+ return "mdi:transmission-tower-export"
+ return "mdi:transmission-tower-off"
+
+
+class TeslaEnergyExportRule(TeslaEnergyEntity, SelectEntity):
+ """Representation of a Tesla energy site energy export rule select."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: SolarPowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ):
+ """Initialize energy export rule entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "energy exports"
+ self._attr_options = EXPORT_RULE
+ self._attr_icon = "mdi:home-export-outline"
+
+ async def async_select_option(self, option: str, **kwargs):
+ """Change the selected option."""
+ if option == EXPORT_RULE[0]:
+ await self._energysite.set_export_rule("pv_only")
+ if option == EXPORT_RULE[1]:
+ await self._energysite.set_export_rule("battery_ok")
+
+ await self.async_update_ha_state()
+
+ @property
+ def current_option(self):
+ """Return current energy export rule setting."""
+ if self._energysite.export_rule == "pv_only":
+ return EXPORT_RULE[0]
+ if self._energysite.export_rule == "battery_ok":
+ return EXPORT_RULE[1]
+
+
+class TeslaEnergyOperationMode(TeslaEnergyEntity, SelectEntity):
+ """Representation of a Tesla energy site operation mode select."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ):
+ """Initialize operation mode entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "operation mode"
+ self._attr_options = OPERATION_MODE
+ self._attr_icon = "mdi:home-battery"
+
+ async def async_select_option(self, option: str, **kwargs):
+ """Change the selected option."""
+ if option == OPERATION_MODE[0]:
+ await self._energysite.set_operation_mode("self_consumption")
+ if option == OPERATION_MODE[1]:
+ await self._energysite.set_operation_mode("autonomous")
+ if option == OPERATION_MODE[2]:
+ await self._energysite.set_operation_mode("backup")
+
+ await self.async_update_ha_state()
+
+ @property
+ def current_option(self):
+ """Return current operation mode setting."""
+ if self._energysite.operation_mode == "self_consumption":
+ return OPERATION_MODE[0]
+ if self._energysite.operation_mode == "autonomous":
+ return OPERATION_MODE[1]
+ if self._energysite.operation_mode == "backup":
+ return OPERATION_MODE[2]
diff --git a/custom_components/tesla_custom/sensor.py b/custom_components/tesla_custom/sensor.py
index 098c034a..35c75662 100644
--- a/custom_components/tesla_custom/sensor.py
+++ b/custom_components/tesla_custom/sensor.py
@@ -1,112 +1,489 @@
"""Support for the Tesla sensors."""
-from __future__ import annotations
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.const import RESOURCE_TYPE_SOLAR, RESOURCE_TYPE_BATTERY
+from teslajsonpy.energy import EnergySite, PowerwallSite
-from homeassistant.components.sensor import DEVICE_CLASSES, STATE_CLASSES, SensorEntity
+from homeassistant.components.sensor import (
+ SensorDeviceClass,
+ SensorEntity,
+ SensorStateClass,
+)
from homeassistant.const import (
+ CONF_UNIT_SYSTEM_METRIC,
+ ENERGY_KILO_WATT_HOUR,
+ ENERGY_WATT_HOUR,
LENGTH_KILOMETERS,
LENGTH_MILES,
+ PERCENTAGE,
+ POWER_WATT,
+ POWER_KILO_WATT,
+ SPEED_KILOMETERS_PER_HOUR,
+ SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
- TEMP_FAHRENHEIT,
)
-from homeassistant.util.distance import convert
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.icon import icon_for_battery_level
+from homeassistant.util.unit_conversion import DistanceConverter
+
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity, TeslaEnergyEntity
+from .const import DOMAIN
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
+SOLAR_SITE_SENSORS = ["solar power", "grid power", "load power"]
+BATTERY_SITE_SENSORS = SOLAR_SITE_SENSORS + ["battery power"]
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"]
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla Sensors by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+ energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
entities = []
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]:
- if device.type == "temperature sensor":
- entities.append(TeslaSensor(device, coordinator, "inside"))
- entities.append(TeslaSensor(device, coordinator, "outside"))
- else:
- entities.append(TeslaSensor(device, coordinator))
+
+ for car in cars.values():
+ entities.append(TeslaCarBattery(hass, car, coordinator))
+ entities.append(TeslaCarChargerRate(hass, car, coordinator))
+ entities.append(TeslaCarChargerEnergy(hass, car, coordinator))
+ entities.append(TeslaCarChargerPower(hass, car, coordinator))
+ entities.append(TeslaCarOdometer(hass, car, coordinator))
+ entities.append(TeslaCarRange(hass, car, coordinator))
+ entities.append(TeslaCarTemp(hass, car, coordinator))
+ entities.append(TeslaCarTemp(hass, car, coordinator, inside=True))
+
+ for energysite in energysites.values():
+ if (
+ energysite.resource_type == RESOURCE_TYPE_SOLAR
+ and energysite.has_load_meter
+ ):
+ for sensor_type in SOLAR_SITE_SENSORS:
+ entities.append(
+ TeslaEnergyPowerSensor(hass, energysite, coordinator, sensor_type)
+ )
+ elif energysite.resource_type == RESOURCE_TYPE_SOLAR:
+ entities.append(
+ TeslaEnergyPowerSensor(hass, energysite, coordinator, "solar power")
+ )
+
+ if energysite.resource_type == RESOURCE_TYPE_BATTERY:
+ entities.append(TeslaEnergyBattery(hass, energysite, coordinator))
+ entities.append(TeslaEnergyBatteryRemaining(hass, energysite, coordinator))
+ entities.append(TeslaEnergyBackupReserve(hass, energysite, coordinator))
+ for sensor_type in BATTERY_SITE_SENSORS:
+ entities.append(
+ TeslaEnergyPowerSensor(hass, energysite, coordinator, sensor_type)
+ )
+
async_add_entities(entities, True)
-class TeslaSensor(TeslaDevice, SensorEntity):
- """Representation of Tesla sensors."""
+class TeslaCarBattery(TeslaCarEntity, SensorEntity):
+ """Representation of the Tesla car battery sensor."""
- def __init__(self, tesla_device, coordinator, sensor_type=None):
- """Initialize of the sensor."""
- super().__init__(tesla_device, coordinator)
- self.type = sensor_type
- if self.type:
- self._name = f"{super().name} ({self.type})"
- self._unique_id = f"{super().unique_id}_{self.type}"
-
- @property
- def native_value(self) -> float | None:
- """Return the native_value of the sensor."""
- if self.tesla_device.type == "temperature sensor":
- if self.type == "outside":
- return self.tesla_device.get_outside_temp()
- return self.tesla_device.get_inside_temp()
- if self.tesla_device.type in ["range sensor", "mileage sensor"]:
- units = self.tesla_device.measurement
- if units == "LENGTH_MILES":
- return self.tesla_device.get_value()
- return round(
- convert(self.tesla_device.get_value(), LENGTH_MILES, LENGTH_KILOMETERS),
- 2,
- )
- if self.tesla_device.type == "charging rate sensor":
- return self.tesla_device.charging_rate
- return self.tesla_device.get_value()
-
- @property
- def native_unit_of_measurement(self) -> str | None:
- """Return the native_unit_of_measurement of the device."""
- units = self.tesla_device.measurement
- if units == "F":
- return TEMP_FAHRENHEIT
- if units == "C":
- return TEMP_CELSIUS
- if units == "LENGTH_MILES":
- return LENGTH_MILES
- if units == "LENGTH_KILOMETERS":
- return LENGTH_KILOMETERS
- return units
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize the Sensor Entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "battery"
+ self._attr_device_class = SensorDeviceClass.BATTERY
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = PERCENTAGE
+ self._attr_icon = "mdi:battery"
+
+ @staticmethod
+ def has_battery() -> bool:
+ """Return whether the device has a battery."""
+ return True
@property
- def device_class(self) -> str | None:
- """Return the device_class of the device."""
- return (
- self.tesla_device.device_class
- if self.tesla_device.device_class in DEVICE_CLASSES
- else None
+ def native_value(self) -> int:
+ """Return battery level."""
+ return self._car.battery_level
+
+ @property
+ def icon(self):
+ """Return icon for the battery."""
+ charging = self._car.battery_level == "Charging"
+
+ return icon_for_battery_level(
+ battery_level=self.native_value, charging=charging
)
+
+class TeslaCarChargerEnergy(TeslaCarEntity, SensorEntity):
+ """Representation of a Tesla car energy added sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize energy added entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "energy added"
+ self._attr_device_class = SensorDeviceClass.ENERGY
+ self._attr_state_class = SensorStateClass.TOTAL_INCREASING
+ self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
+ self._attr_icon = "mdi:lightning-bolt"
+
@property
- def state_class(self) -> str | None:
- """Return the state_class of the device."""
- try:
- return (
- self.tesla_device.state_class
- if self.tesla_device.state_class in STATE_CLASSES
- else None
+ def native_value(self) -> float:
+ """Return the charge energy added."""
+ if self._car.charging_state == "Charging":
+ return self._car.charge_energy_added
+ return "0"
+
+ @property
+ def extra_state_attributes(self):
+ """Return device state attributes."""
+ if self._car.charge_miles_added_rated:
+ added_range = self._car.charge_miles_added_rated
+ elif (
+ self._car.charge_miles_added_ideal
+ and self._car.gui_range_display == "Ideal"
+ ):
+ added_range = self._car.charge_miles_added_ideal
+ else:
+ added_range = 0
+
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ added_range = DistanceConverter.convert(
+ added_range, LENGTH_MILES, LENGTH_KILOMETERS
)
- except AttributeError:
- return None
+
+ return {
+ "added_range": round(added_range, 2),
+ }
+
+
+class TeslaCarChargerPower(TeslaCarEntity, SensorEntity):
+ """Representation of a Tesla car charger power."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize energy added entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charger power"
+ self._attr_device_class = SensorDeviceClass.POWER
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = POWER_KILO_WATT
+
+ @property
+ def native_value(self) -> int:
+ """Return the charger power."""
+ return self._car.charger_power
@property
def extra_state_attributes(self):
- """Return the state attributes of the device."""
- attr = self._attributes.copy()
- if self.tesla_device.type == "charging rate sensor":
- attr.update(
- {
- "time_left": self.tesla_device.time_left,
- "added_range": self.tesla_device.added_range,
- "charge_energy_added": self.tesla_device.charge_energy_added,
- "charge_current_request": self.tesla_device.charge_current_request,
- "charge_current_request_max": self.tesla_device.charge_current_request_max,
- "charger_actual_current": self.tesla_device.charger_actual_current,
- "charger_voltage": self.tesla_device.charger_voltage,
- "charger_power": self.tesla_device.charger_power,
- }
+ """Return device state attributes."""
+ return {
+ "charger_amps_request": self._car.charge_current_request,
+ "charger_amps_actual": self._car.charger_actual_current,
+ "charger_volts": self._car.charger_voltage,
+ "charger_phases": self._car.charger_phases,
+ }
+
+
+class TeslaCarChargerRate(TeslaCarEntity, SensorEntity):
+ """Representation of the Tesla car charging rate."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charging rate entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charging rate"
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_icon = "mdi:speedometer"
+
+ @property
+ def native_value(self) -> float:
+ """Return charge rate."""
+ charge_rate = self._car.charge_rate
+
+ if charge_rate is None:
+ return charge_rate
+
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ charge_rate = DistanceConverter.convert(
+ charge_rate, LENGTH_MILES, LENGTH_KILOMETERS
)
- return attr
+
+ return round(charge_rate, 2)
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return distance units."""
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ return SPEED_KILOMETERS_PER_HOUR
+
+ return SPEED_MILES_PER_HOUR
+
+ @property
+ def extra_state_attributes(self):
+ """Return device state attributes."""
+ return {
+ "time_left": self._car.time_to_full_charge,
+ }
+
+
+class TeslaCarOdometer(TeslaCarEntity, SensorEntity):
+ """Representation of the Tesla car odometer sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize odometer entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "odometer"
+ self._attr_device_class = None
+ self._attr_state_class = SensorStateClass.TOTAL_INCREASING
+ self._attr_icon = "mdi:counter"
+
+ @property
+ def native_value(self) -> float:
+ """Return the odometer."""
+ odometer_value = self._car.odometer
+
+ if odometer_value is None:
+ return None
+
+ if self.native_unit_of_measurement == LENGTH_KILOMETERS:
+ odometer_value = DistanceConverter.convert(
+ odometer_value, LENGTH_MILES, LENGTH_KILOMETERS
+ )
+
+ return round(odometer_value, 2)
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return distance units."""
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ return LENGTH_KILOMETERS
+
+ return LENGTH_MILES
+
+
+class TeslaCarRange(TeslaCarEntity, SensorEntity):
+ """Representation of the Tesla car range sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize range entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "range"
+ self._attr_device_class = None
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_icon = "mdi:gauge"
+
+ @property
+ def native_value(self) -> float:
+ """Return range."""
+ range_value = self._car.battery_range
+
+ if self._car.gui_range_display == "Ideal":
+ range_value = self._car.ideal_battery_range
+
+ if range_value is None:
+ return None
+
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ range_value = DistanceConverter.convert(
+ range_value, LENGTH_MILES, LENGTH_KILOMETERS
+ )
+
+ return round(range_value, 2)
+
+ @property
+ def native_unit_of_measurement(self) -> str:
+ """Return distance units."""
+ if self._unit_system == CONF_UNIT_SYSTEM_METRIC:
+ return LENGTH_KILOMETERS
+
+ return LENGTH_MILES
+
+
+class TeslaCarTemp(TeslaCarEntity, SensorEntity):
+ """Representation of a Tesla car temp sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ *,
+ inside=False,
+ ) -> None:
+ """Initialize temp entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "temperature"
+ self.inside = inside
+
+ if inside is True:
+ self.type += " (inside)"
+ else:
+ self.type += " (outside)"
+
+ self._attr_device_class = SensorDeviceClass.TEMPERATURE
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = TEMP_CELSIUS
+ self._attr_icon = "mdi:thermometer"
+
+ @property
+ def native_value(self) -> float:
+ """Return car temperature."""
+ if self.inside is True:
+ return self._car.inside_temp
+
+ return self._car.outside_temp
+
+
+class TeslaEnergyPowerSensor(TeslaEnergyEntity, SensorEntity):
+ """Representation of a Tesla energy power sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: EnergySite,
+ coordinator: TeslaDataUpdateCoordinator,
+ sensor_type: str,
+ ) -> None:
+ """Initialize power sensor."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = sensor_type
+ self._attr_device_class = SensorDeviceClass.POWER
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = POWER_WATT
+
+ if self.type == "solar power":
+ self._attr_icon = "mdi:solar-power-variant"
+ if self.type == "grid power":
+ self._attr_icon = "mdi:transmission-tower"
+ if self.type == "load power":
+ self._attr_icon = "mdi:home-lightning-bolt"
+ if self.type == "battery power":
+ self._attr_icon = "mdi:home-battery"
+
+ @property
+ def native_value(self) -> float:
+ """Return power in Watts."""
+ if self.type == "solar power":
+ return round(self._energysite.solar_power)
+ if self.type == "grid power":
+ return round(self._energysite.grid_power)
+ if self.type == "load power":
+ return round(self._energysite.load_power)
+ if self.type == "battery power":
+ return round(self._energysite.battery_power)
+
+
+class TeslaEnergyBattery(TeslaEnergyEntity, SensorEntity):
+ """Representation of the Tesla energy battery sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize battery sensor entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "battery"
+ self._attr_device_class = SensorDeviceClass.BATTERY
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = PERCENTAGE
+
+ @staticmethod
+ def has_battery() -> bool:
+ """Return whether the device has a battery."""
+ return True
+
+ @property
+ def native_value(self) -> int:
+ """Return battery level."""
+ return round(self._energysite.percentage_charged)
+
+ @property
+ def icon(self):
+ """Return icon for the battery."""
+ charging = self._energysite.battery_power < -100
+
+ return icon_for_battery_level(
+ battery_level=self.native_value, charging=charging
+ )
+
+
+class TeslaEnergyBatteryRemaining(TeslaEnergyEntity, SensorEntity):
+ """Representation of a Tesla energy battery remaining sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize battery remaining entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "battery remaining"
+ self._attr_device_class = SensorDeviceClass.BATTERY
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = ENERGY_WATT_HOUR
+
+ @property
+ def native_value(self) -> int:
+ """Return battery energy remaining."""
+ return round(self._energysite.energy_left)
+
+ @property
+ def icon(self):
+ """Return icon for the battery remaining."""
+ charging = self._energysite.battery_power < -100
+
+ return icon_for_battery_level(
+ battery_level=self._energysite.percentage_charged, charging=charging
+ )
+
+
+class TeslaEnergyBackupReserve(TeslaEnergyEntity, SensorEntity):
+ """Representation of a Tesla energy backup reserve sensor."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ energysite: PowerwallSite,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize backup energy reserve entity."""
+ super().__init__(hass, energysite, coordinator)
+ self.type = "backup reserve"
+ self._attr_device_class = SensorDeviceClass.BATTERY
+ self._attr_state_class = SensorStateClass.MEASUREMENT
+ self._attr_native_unit_of_measurement = PERCENTAGE
+
+ @property
+ def native_value(self) -> int:
+ """Return backup reserve level."""
+ return round(self._energysite.backup_reserve_percent)
+
+ @property
+ def icon(self):
+ """Return icon for the backup reserve."""
+ return icon_for_battery_level(battery_level=self.native_value)
diff --git a/custom_components/tesla_custom/services.py b/custom_components/tesla_custom/services.py
index 8659fb8a..0dc923dc 100644
--- a/custom_components/tesla_custom/services.py
+++ b/custom_components/tesla_custom/services.py
@@ -2,27 +2,23 @@
SPDX-License-Identifier: Apache-2.0
"""
-
import logging
-from homeassistant.const import CONF_EMAIL
+import voluptuous as vol
from teslajsonpy import Controller
+
+from homeassistant.const import ATTR_COMMAND, CONF_EMAIL, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
-import voluptuous as vol
-from homeassistant.const import (
- ATTR_COMMAND,
- CONF_SCAN_INTERVAL,
-)
from .const import (
ATTR_PARAMETERS,
ATTR_PATH_VARS,
ATTR_VIN,
+ DEFAULT_SCAN_INTERVAL,
DOMAIN,
SERVICE_API,
SERVICE_SCAN_INTERVAL,
- DEFAULT_SCAN_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
@@ -40,10 +36,13 @@
{
vol.Optional(CONF_EMAIL): vol.All(cv.string, vol.Length(min=1)),
vol.Optional(ATTR_VIN): vol.All(cv.string, vol.Length(min=1)),
- vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=-1, max=3600)),
+ vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(
+ vol.Coerce(int), vol.Range(min=-1, max=3600)
+ ),
}
)
+
@callback
def async_setup_services(hass) -> None:
"""Set up services for Tesla integration."""
@@ -155,11 +154,12 @@ async def set_update_interval(call):
"Changing update_interval from %s to %s for %s",
old_update_interval,
update_interval,
- vin
+ vin,
)
controller.set_update_interval_vin(vin=vin, value=update_interval)
return True
+
@callback
def async_unload_services(hass) -> None:
"""Unload Tesla services."""
diff --git a/custom_components/tesla_custom/strings.json b/custom_components/tesla_custom/strings.json
index 229fe54b..a5cc58e3 100644
--- a/custom_components/tesla_custom/strings.json
+++ b/custom_components/tesla_custom/strings.json
@@ -15,9 +15,11 @@
"mfa": "MFA Code (optional)",
"password": "Password",
"username": "Email",
- "token": "Refresh Token"
+ "token": "Refresh Token",
+ "include_vehicles": "Include Vehicles",
+ "include_energysites": "Include Energy Sites"
},
- "description": "Use 'Auth App for Tesla' on iOS, 'Tesla Tokens' on Android\r\n or 'telsafi.com' to create a refresh token and enter it below.",
+ "description": "Use 'Auth App for Tesla' on iOS, 'Tesla Tokens' on Android\r\n or 'telsafi.com' to create a refresh token and enter it below.\r\n Vehicle(s) are forced awake for setup.",
"title": "Tesla - Configuration"
}
}
@@ -32,4 +34,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/custom_components/tesla_custom/switch.py b/custom_components/tesla_custom/switch.py
index f0d5b003..e1242dea 100644
--- a/custom_components/tesla_custom/switch.py
+++ b/custom_components/tesla_custom/switch.py
@@ -1,178 +1,163 @@
-"""Support for Tesla charger switches."""
-from custom_components.tesla_custom.const import ICONS
+"""Support for Tesla switches."""
import logging
+from teslajsonpy.car import TeslaCar
+
from homeassistant.components.switch import SwitchEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
-from . import DOMAIN as TESLA_DOMAIN
-from .tesla_device import TeslaDevice
-from .helpers import wait_for_climate
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the Tesla binary_sensors by config_entry."""
- coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"]
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla switches by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entities = []
- for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]:
- if device.type == "charger switch":
- entities.append(ChargerSwitch(device, coordinator))
- entities.append(UpdateSwitch(device, coordinator))
- elif device.type == "maxrange switch":
- entities.append(RangeSwitch(device, coordinator))
- elif device.type == "sentry mode switch":
- entities.append(SentryModeSwitch(device, coordinator))
- elif device.type == "heated steering switch":
- entities.append(HeatedSteeringWheelSwitch(device, coordinator))
- async_add_entities(entities, True)
-
-class HeatedSteeringWheelSwitch(TeslaDevice, SwitchEntity):
- """Representation of a Tesla Heated Steering Wheel switch."""
+ for car in cars.values():
+ if car.steering_wheel_heater:
+ entities.append(TeslaCarHeatedSteeringWheel(hass, car, coordinator))
+ if car.sentry_mode_available:
+ entities.append(TeslaCarSentryMode(hass, car, coordinator))
+ entities.append(TeslaCarPolling(hass, car, coordinator))
+ entities.append(TeslaCarCharger(hass, car, coordinator))
- @TeslaDevice.Decorators.check_for_reauth
- async def async_turn_on(self, **kwargs):
- """Send the on command."""
- _LOGGER.debug("Turn on Heating Steering Wheel: %s", self.name)
+ async_add_entities(entities, True)
- vin = self.tesla_device.vin()
- await wait_for_climate(self.hass, self.config_entry_id, vin)
- await self.tesla_device.set_steering_wheel_heat(True)
- self.async_write_ha_state()
+class TeslaCarHeatedSteeringWheel(TeslaCarEntity, SwitchEntity):
+ """Representation of a Tesla car heated steering wheel switch."""
- @TeslaDevice.Decorators.check_for_reauth
- async def async_turn_off(self, **kwargs):
- """Send the off command."""
- _LOGGER.debug("Turn off Heating Steering Wheel: %s", self.name)
- await self.tesla_device.set_steering_wheel_heat(False)
- self.async_write_ha_state()
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize heated steering wheel entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "heated steering"
+ self._attr_icon = "mdi:steering"
@property
def is_on(self):
- """Get whether the switch is in on state."""
- return self.tesla_device.get_steering_wheel_heat()
+ """Return True if steering wheel heater is on."""
+ return self._car.is_steering_wheel_heater_on
- @property
- def icon(self):
- """Return the icon of the sensor."""
- return ICONS.get("heated steering wheel")
-
-
-class ChargerSwitch(TeslaDevice, SwitchEntity):
- """Representation of a Tesla charger switch."""
-
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_on(self, **kwargs):
"""Send the on command."""
- _LOGGER.debug("Enable charging: %s", self.name)
- await self.tesla_device.start_charge()
- self.async_write_ha_state()
+ await self._car.set_heated_steering_wheel(True)
+ await self.async_update_ha_state()
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_off(self, **kwargs):
"""Send the off command."""
- _LOGGER.debug("Disable charging for: %s", self.name)
- await self.tesla_device.stop_charge()
- self.async_write_ha_state()
+ await self._car.set_heated_steering_wheel(False)
+ await self.async_update_ha_state()
+
+
+class TeslaCarPolling(TeslaCarEntity, SwitchEntity):
+ """Representation of a polling switch."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize polling entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "polling"
+ self._attr_icon = "mdi:car-connected"
+ self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self):
- """Get whether the switch is in on state."""
- if self.tesla_device.is_charging() is None:
+ """Return True if updates available."""
+ if self._coordinator.controller.get_updates(vin=self._car.vin) is None:
return None
- return self.tesla_device.is_charging()
+ return bool(self._coordinator.controller.get_updates(vin=self._car.vin))
-class RangeSwitch(TeslaDevice, SwitchEntity):
- """Representation of a Tesla max range charging switch."""
-
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_on(self, **kwargs):
"""Send the on command."""
- _LOGGER.debug("Enable max range charging: %s", self.name)
- await self.tesla_device.set_max()
- self.async_write_ha_state()
+ _LOGGER.debug("Enable polling: %s %s", self.name, self._car.vin)
+ self._coordinator.controller.set_updates(vin=self._car.vin, value=True)
+ await self.async_update_ha_state()
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_off(self, **kwargs):
"""Send the off command."""
- _LOGGER.debug("Disable max range charging: %s", self.name)
- await self.tesla_device.set_standard()
- self.async_write_ha_state()
+ _LOGGER.debug("Disable polling: %s %s", self.name, self._car.vin)
+ self._coordinator.controller.set_updates(vin=self._car.vin, value=False)
+ await self.async_update_ha_state()
- @property
- def is_on(self):
- """Get whether the switch is in on state."""
- if self.tesla_device.is_maxrange() is None:
- return None
- return bool(self.tesla_device.is_maxrange())
-
-
-class UpdateSwitch(TeslaDevice, SwitchEntity):
- """Representation of a Tesla update switch. Described in UI as polling."""
- def __init__(self, tesla_device, coordinator):
- """Initialise the switch."""
- super().__init__(tesla_device, coordinator)
- self.controller = coordinator.controller
+class TeslaCarCharger(TeslaCarEntity, SwitchEntity):
+ """Representation of a Tesla car charger switch."""
- @property
- def name(self):
- """Return the name of the device."""
- return super().name.replace("charger", "polling")
-
- @property
- def icon(self):
- """Return the icon of the sensor."""
- return ICONS.get("update switch")
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize charger switch entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "charger"
+ self._attr_icon = "mdi:ev-station"
@property
- def unique_id(self) -> str:
- """Return a unique ID."""
- return super().unique_id.replace("charger", "update")
+ def is_on(self):
+ """Return charging state."""
+ return self._car.charging_state == "Charging"
async def async_turn_on(self, **kwargs):
"""Send the on command."""
- _LOGGER.debug("Enable polling: %s %s", self.name, self.tesla_device.id())
- self.controller.set_updates(car_id=self.tesla_device.id(), value=True)
- self.async_write_ha_state()
+ await self._car.start_charge()
+ await self.async_update_ha_state()
async def async_turn_off(self, **kwargs):
"""Send the off command."""
- _LOGGER.debug("Disable polling: %s %s", self.name, self.tesla_device.id())
- self.controller.set_updates(car_id=self.tesla_device.id(), value=False)
- self.async_write_ha_state()
+ await self._car.stop_charge()
+ await self.async_update_ha_state()
+
+
+class TeslaCarSentryMode(TeslaCarEntity, SwitchEntity):
+ """Representation of a Tesla car sentry mode switch."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize sentry mode entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "sentry mode"
+ self._attr_icon = "mdi:shield-car"
@property
def is_on(self):
- """Get whether the switch is in on state."""
- if self.controller.get_updates(self.tesla_device.id()) is None:
- return None
- return bool(self.controller.get_updates(self.tesla_device.id()))
+ """Return True if sentry mode is on."""
+ sentry_mode_available = self._car.sentry_mode_available
+ sentry_mode_status = self._car.sentry_mode
+ if sentry_mode_available is True and sentry_mode_status is True:
+ return True
-class SentryModeSwitch(TeslaDevice, SwitchEntity):
- """Representation of a Tesla sentry mode switch."""
+ return False
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_on(self, **kwargs):
"""Send the on command."""
- _LOGGER.debug("Enable sentry mode: %s", self.name)
- await self.tesla_device.enable_sentry_mode()
- self.async_write_ha_state()
+ await self._car.set_sentry_mode(True)
+ await self.async_update_ha_state()
- @TeslaDevice.Decorators.check_for_reauth
async def async_turn_off(self, **kwargs):
"""Send the off command."""
- _LOGGER.debug("Disable sentry mode: %s", self.name)
- await self.tesla_device.disable_sentry_mode()
- self.async_write_ha_state()
-
- @property
- def is_on(self):
- """Get whether the switch is in on state."""
- if self.tesla_device.is_on() is None:
- return None
- return self.tesla_device.is_on()
+ await self._car.set_sentry_mode(False)
+ await self.async_update_ha_state()
diff --git a/custom_components/tesla_custom/tesla_device.py b/custom_components/tesla_custom/tesla_device.py
deleted file mode 100644
index f8748ccf..00000000
--- a/custom_components/tesla_custom/tesla_device.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""Support for Tesla cars."""
-from functools import wraps
-import logging
-from typing import Any, Optional, Tuple
-
-from homeassistant.const import ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL
-from homeassistant.core import callback
-from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from homeassistant.util import slugify
-from teslajsonpy.exceptions import IncompleteCredentials
-
-from .const import DOMAIN, ICONS
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def device_identifier(tesla_device) -> Tuple[str, int]:
- """Return the identifier for a tesla device."""
- # Note that Home Assistant types this to be
- # tuple[str, str] but since that would involve
- # migrating, it is not changed here.
- return (DOMAIN, tesla_device.id())
-
-
-class TeslaDevice(CoordinatorEntity):
- """Representation of a Tesla device."""
-
- class Decorators(CoordinatorEntity):
- """Decorators for Tesla Devices."""
-
- @classmethod
- def check_for_reauth(cls, func):
- """Wrap a Tesla device function to check for need to reauthenticate."""
-
- @wraps(func)
- async def wrapped(*args, **kwargs):
- result: Any = None
- self_object: Optional[TeslaDevice] = None
- if isinstance(args[0], TeslaDevice):
- self_object = args[0]
- try:
- result = await func(*args, **kwargs)
- except IncompleteCredentials:
- if self_object and self_object.config_entry_id:
- _LOGGER.debug(
- "Reauth needed for %s after calling: %s",
- self_object,
- func,
- )
- await self_object.hass.config_entries.async_reload(
- self_object.config_entry_id
- )
- return None
- return result
-
- return wrapped
-
- def __init__(self, tesla_device, coordinator):
- """Initialise the Tesla device."""
- super().__init__(coordinator)
- self.tesla_device = tesla_device
- self._name: str = self.tesla_device.name
- self._unique_id: str = slugify(self.tesla_device.uniq_name)
- self._attributes: str = self.tesla_device.attrs.copy()
- self.config_entry_id: Optional[str] = None
- self._attr_entity_registry_enabled_default = (
- self.tesla_device.enabled_by_default
- )
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def unique_id(self) -> str:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def icon(self):
- """Return the icon of the sensor."""
- if self.device_class:
- return None
-
- return ICONS.get(self.tesla_device.type)
-
- @property
- def extra_state_attributes(self):
- """Return the state attributes of the device."""
- attr = self._attributes
- if self.tesla_device.has_battery():
- attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level()
- attr[ATTR_BATTERY_CHARGING] = self.tesla_device.battery_charging()
- return attr
-
- @property
- def device_info(self):
- """Return the device_info of the device."""
- if hasattr(self.tesla_device, "car_name"):
- return {
- "identifiers": {device_identifier(self.tesla_device)},
- "name": self.tesla_device.car_name(),
- "manufacturer": "Tesla",
- "model": self.tesla_device.car_type,
- "sw_version": self.tesla_device.car_version,
- }
- elif hasattr(self.tesla_device, "site_name"):
- return {
- "identifiers": {device_identifier(self.tesla_device)},
- "name": self.tesla_device.site_name(),
- "manufacturer": "Tesla",
- }
- return None
-
- async def async_added_to_hass(self):
- """Register state update callback."""
- self.async_on_remove(self.coordinator.async_add_listener(self.refresh))
- registry = er.async_get(self.hass)
- self.config_entry_id = registry.entities.get(self.entity_id).config_entry_id
-
- @callback
- def refresh(self) -> None:
- """Refresh the state of the device.
-
- This assumes the coordinator has updated the controller.
- """
- self.tesla_device.refresh()
- self._attributes = self.tesla_device.attrs.copy()
- self.async_write_ha_state()
diff --git a/custom_components/tesla_custom/translations/en.json b/custom_components/tesla_custom/translations/en.json
index d8a1f793..a5cc58e3 100644
--- a/custom_components/tesla_custom/translations/en.json
+++ b/custom_components/tesla_custom/translations/en.json
@@ -15,9 +15,11 @@
"mfa": "MFA Code (optional)",
"password": "Password",
"username": "Email",
- "token": "Refresh Token"
+ "token": "Refresh Token",
+ "include_vehicles": "Include Vehicles",
+ "include_energysites": "Include Energy Sites"
},
- "description": "Use 'Auth App for Tesla' on iOS or 'Tesla Tokens' on Android\r\n to create a refresh token and enter it below.",
+ "description": "Use 'Auth App for Tesla' on iOS, 'Tesla Tokens' on Android\r\n or 'telsafi.com' to create a refresh token and enter it below.\r\n Vehicle(s) are forced awake for setup.",
"title": "Tesla - Configuration"
}
}
@@ -27,10 +29,9 @@
"init": {
"data": {
"enable_wake_on_start": "Force cars awake on startup",
- "scan_interval": "Seconds between polling",
- "polling_policy": "Polling policy. See https://github.com/alandtse/tesla/wiki/Polling-policy"
+ "scan_interval": "Seconds between polling"
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/custom_components/tesla_custom/update.py b/custom_components/tesla_custom/update.py
new file mode 100644
index 00000000..ff946398
--- /dev/null
+++ b/custom_components/tesla_custom/update.py
@@ -0,0 +1,108 @@
+"""Support for Tesla update."""
+from typing import Any
+
+from teslajsonpy.car import TeslaCar
+
+from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
+from homeassistant.core import HomeAssistant
+
+from . import TeslaDataUpdateCoordinator
+from .base import TeslaCarEntity
+from .const import DOMAIN
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
+ """Set up the Tesla update entities by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
+ cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
+
+ entities = [
+ TeslaCarUpdate(
+ hass,
+ car,
+ coordinator,
+ )
+ for car in cars.values()
+ ]
+ async_add_entities(entities, True)
+
+
+class TeslaCarUpdate(TeslaCarEntity, UpdateEntity):
+ """Representation of a Tesla car update."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ car: TeslaCar,
+ coordinator: TeslaDataUpdateCoordinator,
+ ) -> None:
+ """Initialize update entity."""
+ super().__init__(hass, car, coordinator)
+ self.type = "software update"
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
+
+ @property
+ def release_url(self) -> str:
+ """Return release URL.
+
+ Uses notateslaapp.com as Tesla doesn't have offical web based release Notes.
+ """
+ version_str = self.latest_version
+ if version_str is None:
+ version_str = self.installed_version
+
+ if version_str is None:
+ return None
+
+ return f"https://www.notateslaapp.com/software-updates/version/{version_str}/release-notes"
+
+ @property
+ def latest_version(self) -> str:
+ """Get the latest version."""
+ version_str = None
+
+ if self._car.software_update:
+ version_str: str = self._car.software_update.get("version")
+ # If we don't have a software_update version, then we're running the latest version.
+ if version_str is None or version_str.strip() == "":
+ version_str = self.installed_version
+
+ return version_str
+
+ @property
+ def installed_version(self) -> str:
+ """Get the installed version."""
+ version_str = self._car.car_version
+ # We will split out the version Hash, purely cause it looks nicer in the UI.
+ if version_str is not None:
+ version_str = version_str.split(" ")[0]
+
+ return version_str
+
+ @property
+ def in_progress(self):
+ """Get Progress, if updating."""
+ update_status = None
+
+ if self._car.software_update:
+ update_status = self._car.software_update.get("status")
+ # If the update is scheduled, then its Simply In Progress
+ if update_status == "scheduled":
+ return True
+ # If its actually installing, we can use the install_perc
+ if update_status == "installing":
+ progress = self._car.software_update.get("install_perc")
+ return progress
+ # Otherwise, we're not updating, so return False
+ return False
+
+ async def async_install(self, version, backup: bool, **kwargs: Any) -> None:
+ """Install an Update."""
+ # Ask Tesla to start the update now.
+ await self._car.schedule_software_update(offset_sec=0)
+ # Do a controller refresh, to get the latest data from Tesla.
+ await self.update_controller(force=True)
diff --git a/hacs.json b/hacs.json
index 321430f7..c11577af 100644
--- a/hacs.json
+++ b/hacs.json
@@ -1,7 +1,7 @@
{
"name": "Tesla",
"hacs": "1.6.0",
- "homeassistant": "2021.9.0",
+ "homeassistant": "2022.10.0",
"zip_release": true,
"filename": "tesla_custom.zip"
}
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 333f843b..1463c64a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -78,7 +78,6 @@ python-versions = ">=3.7.2"
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
-typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
wrapt = [
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
@@ -210,7 +209,6 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
@@ -220,7 +218,7 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "bleak"
-version = "0.18.1"
+version = "0.19.0"
description = "Bluetooth Low Energy platform Agnostic Klient"
category = "dev"
optional = false
@@ -228,11 +226,11 @@ python-versions = ">=3.7,<4.0"
[package.dependencies]
async-timeout = ">=3.0.0,<5"
-bleak-winrt = {version = ">=1.1.1,<2.0.0", markers = "platform_system == \"Windows\""}
-dbus-fast = {version = ">=1.4.0,<2.0.0", markers = "platform_system == \"Linux\""}
-pyobjc-core = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""}
-pyobjc-framework-CoreBluetooth = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""}
-pyobjc-framework-libdispatch = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""}
+bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\""}
+dbus-fast = {version = ">=1.22.0,<2.0.0", markers = "platform_system == \"Linux\""}
+pyobjc-core = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""}
+pyobjc-framework-CoreBluetooth = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""}
+pyobjc-framework-libdispatch = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""}
[[package]]
name = "bleak-winrt"
@@ -342,7 +340,7 @@ test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests",
[[package]]
name = "dbus-fast"
-version = "1.38.0"
+version = "1.45.0"
description = "A faster version of dbus-next"
category = "dev"
optional = false
@@ -468,7 +466,7 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.28"
+version = "3.1.29"
description = "GitPython is a python library used to interact with Git repositories"
category = "dev"
optional = false
@@ -479,7 +477,7 @@ gitdb = ">=4.0.1,<5"
[[package]]
name = "greenlet"
-version = "1.1.3"
+version = "1.1.3.post0"
description = "Lightweight in-process concurrent programming"
category = "dev"
optional = false
@@ -509,7 +507,7 @@ bleak = ">=0.14.3"
[[package]]
name = "homeassistant"
-version = "2022.10.2"
+version = "2022.10.4"
description = "Open-source home automation platform running on Python 3."
category = "dev"
optional = false
@@ -849,8 +847,8 @@ optional = false
python-versions = ">=3.6"
[package.extras]
-testing = ["pytest-benchmark", "pytest"]
-dev = ["tox", "pre-commit"]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
@@ -978,7 +976,6 @@ mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
-typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
@@ -1133,7 +1130,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
-testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-forked"
@@ -1161,7 +1158,7 @@ pytest = ">=3.0.0"
[[package]]
name = "pytest-homeassistant-custom-component"
-version = "0.12.7"
+version = "0.12.9"
description = "Experimental package to automatically extract test plugins for Home Assistant custom components"
category = "dev"
optional = false
@@ -1171,7 +1168,7 @@ python-versions = ">=3.9"
coverage = "6.4.4"
fnvhash = "0.1.0"
freezegun = "1.2.1"
-homeassistant = "2022.10.2"
+homeassistant = "2022.10.4"
mock-open = "1.4.0"
numpy = "1.23.2"
paho-mqtt = "1.6.1"
@@ -1466,7 +1463,7 @@ develop = ["sphinx"]
[[package]]
name = "stevedore"
-version = "4.0.0"
+version = "4.0.1"
description = "Manage dynamic plugins for Python applications"
category = "dev"
optional = false
@@ -1488,7 +1485,7 @@ tests = ["pytest-cov", "pytest"]
[[package]]
name = "teslajsonpy"
-version = "2.4.5"
+version = "3.0.0"
description = "A library to work with Tesla API."
category = "main"
optional = false
@@ -1659,8 +1656,8 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8
[metadata]
lock-version = "1.1"
-python-versions = "^3.9"
-content-hash = "3a2b139ab3153ceb957fe1f47f91a405489a65570390cdb15ebf40fd19330510"
+python-versions = "^3.10"
+content-hash = "09ff44867e639e0e12354eeabf4d3804b145c9070558d174eaca08c594628d1f"
[metadata.files]
aiohttp = [
@@ -1737,10 +1734,7 @@ aiohttp = [
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
]
-aiohttp-cors = [
- {file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"},
- {file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"},
-]
+aiohttp-cors = []
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
@@ -1749,188 +1743,40 @@ anyio = [
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
]
-astral = [
- {file = "astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a"},
- {file = "astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe"},
-]
-astroid = [
- {file = "astroid-2.12.11-py3-none-any.whl", hash = "sha256:867a756bbf35b7bc07b35bfa6522acd01f91ad9919df675e8428072869792dce"},
- {file = "astroid-2.12.11.tar.gz", hash = "sha256:2df4f9980c4511474687895cbfdb8558293c1a826d9118bb09233d7c2bff1c83"},
-]
+astral = []
+astroid = []
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
-atomicwrites-homeassistant = [
- {file = "atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f"},
- {file = "atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d"},
-]
-attrs = [
- {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
- {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
-]
+atomicwrites-homeassistant = []
+attrs = []
authcaptureproxy = [
{file = "authcaptureproxy-1.1.4-py3-none-any.whl", hash = "sha256:efd1a3077957e3d0269dacbc7c1252b47c4f711e22ce1474ec86f5e1cd584d60"},
{file = "authcaptureproxy-1.1.4.tar.gz", hash = "sha256:6777309a96734076da3c51e8f21d716b785af7d14fe76e90738e6a2c6300d3e4"},
]
-awesomeversion = [
- {file = "awesomeversion-22.9.0-py3-none-any.whl", hash = "sha256:f4716e1e65ea1194be03f312f2b2643a8b76326c59538ddc5353642616ead82a"},
- {file = "awesomeversion-22.9.0.tar.gz", hash = "sha256:2f4190d333e81e10b2a4e156150ddb3596f5f11da67e9d51ba39057aa7a17f7e"},
-]
-backoff = [
- {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
- {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
-]
+awesomeversion = []
+backoff = []
bandit = [
{file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"},
{file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"},
]
-bcrypt = [
- {file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"},
- {file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"},
- {file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"},
- {file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"},
- {file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"},
- {file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"},
- {file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"},
- {file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"},
- {file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"},
- {file = "bcrypt-3.1.7-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:436a487dec749bca7e6e72498a75a5fa2433bda13bac91d023e18df9089ae0b8"},
- {file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"},
- {file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"},
- {file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"},
- {file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"},
- {file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"},
- {file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"},
- {file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"},
- {file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"},
- {file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"},
-]
+bcrypt = []
beautifulsoup4 = [
{file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
]
-black = [
- {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
- {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
- {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
- {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
- {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
- {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
- {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
- {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
- {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
- {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
- {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
- {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
- {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
- {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
- {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
- {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
- {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
- {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
- {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
- {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
- {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
-]
-bleak = [
- {file = "bleak-0.18.1-py3-none-any.whl", hash = "sha256:d6c3d81a03ec92d19e504de7f92f903520dbae38b823433aadc85e9c43cbdab7"},
- {file = "bleak-0.18.1.tar.gz", hash = "sha256:5878871f7ba36ad49183e37e49856d3b165c012b4d3065fbeb477ddba26c3922"},
-]
-bleak-winrt = [
- {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"},
- {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"},
- {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"},
- {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"},
- {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"},
- {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"},
- {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"},
- {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"},
- {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"},
- {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"},
- {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"},
-]
-certifi = [
- {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
- {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
-]
-cffi = [
- {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
- {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
- {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
- {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
- {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
- {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
- {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
- {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
- {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
- {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
- {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
- {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
- {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
- {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
- {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
- {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
- {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
- {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
- {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
- {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
- {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
- {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
- {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
- {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
- {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
- {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
- {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
- {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
- {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
- {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
- {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
- {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
- {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
- {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
- {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
-]
+black = []
+bleak = []
+bleak-winrt = []
+certifi = []
+cffi = []
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
-charset-normalizer = [
- {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
- {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
-]
-ciso8601 = [
- {file = "ciso8601-2.2.0.tar.gz", hash = "sha256:14ad817ed31a698372d42afa81b0173d71cd1d0b48b7499a2da2a01dcc8695e6"},
-]
+charset-normalizer = []
+ciso8601 = []
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
@@ -1939,288 +1785,34 @@ colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
-coverage = [
- {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"},
- {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"},
- {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"},
- {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"},
- {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"},
- {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"},
- {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"},
- {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"},
- {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"},
- {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"},
- {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"},
- {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"},
- {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"},
- {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"},
- {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"},
- {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"},
- {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"},
- {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"},
- {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"},
- {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"},
-]
-cryptography = [
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"},
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"},
- {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"},
- {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"},
- {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"},
-]
-dbus-fast = [
- {file = "dbus-fast-1.38.0.tar.gz", hash = "sha256:25e88ffafbe979c9d6e72dda884c3a04caca7e0fab890f65ec1947e8d1a19d26"},
- {file = "dbus_fast-1.38.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b43f317da30c2716d6e2d74dad03818964fa1d2e3f4ffb73644f07f951139f29"},
- {file = "dbus_fast-1.38.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:569a6af80033940e1ca8bff4965924222a7b63a98ce1cb2a92f6480496bb0469"},
- {file = "dbus_fast-1.38.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:086cb2e26481a77bbb5a57403d2a9df002c387177e5df57d3a4f0d9885ad67e2"},
- {file = "dbus_fast-1.38.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4337144610f80452b6e0238d36b97926bbb72c378ee80306a43b4248f4da5dee"},
- {file = "dbus_fast-1.38.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3d25f5b9dd98a2266f3977e595a8bca69d618e1c20a1b803c4d915218e12cd01"},
- {file = "dbus_fast-1.38.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610b169fb947eb8a91635a5a90f596c7e0ba53df19ee520ea89e13b89395177b"},
- {file = "dbus_fast-1.38.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3166543a7f415b2358f478a5e824f568788b8dae5c1c42df41b7948e5b4e2050"},
- {file = "dbus_fast-1.38.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e974dd7b74ca41c4393561cc0c41b3faf7f5c9dccebb48e18a693821f24f467a"},
- {file = "dbus_fast-1.38.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a07495127f10bff14e5546ebdac617e83bf796aa16c6b539e6be9d8b110baece"},
- {file = "dbus_fast-1.38.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7ab511c6397db13f97bf64f8c73014fb629738d76ee7786e69c547dec950ff"},
- {file = "dbus_fast-1.38.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7f69e082b9bfd9c37c2c0a0185be6d7ef5c3b38747af798695a9b5039133a1e9"},
- {file = "dbus_fast-1.38.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5b6b262b2e7ebebeb49f6e7c47f47f4aae204bffb462119629e3ea4609aa26c9"},
- {file = "dbus_fast-1.38.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6400eb71ba6ff0259b3b5282af32ff2fe984effab8e8e2e33775526d547491da"},
- {file = "dbus_fast-1.38.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1427aed88d9155332a46f65013351c2724a963fb6d5c9e0cc0cc4d9314337cc7"},
- {file = "dbus_fast-1.38.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1958ef8b5158fc4625f266b8ddfa6744d646b08ef20e85497af90d0bc2399b31"},
- {file = "dbus_fast-1.38.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:954f8b7ee664fa15ee8b09b9a16e7d3d0a1ed83fecd9e64777fd17f39773533f"},
- {file = "dbus_fast-1.38.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:488e6f8b03209df3c62bf9131b7b92d05dbd161c624797c7b721f75416be80c7"},
- {file = "dbus_fast-1.38.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11cd4f3a905fb9b6881487edcf316165a61ecde7f4ad162eeba7c6f936bf4d74"},
- {file = "dbus_fast-1.38.0-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:07b0c8e1dcfa61bfd2ef917121c2ce4078a995d2fd7fee7525fd82300c653397"},
- {file = "dbus_fast-1.38.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b05f852b2eb476d129ca5f355bb7b868c3855082634efe371616127e69aa832"},
- {file = "dbus_fast-1.38.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ace8512dadab5571833d48a0375aaaddb01fb23efea9fd6ef3e8d821a37e729c"},
- {file = "dbus_fast-1.38.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4aa2aed839c0091a34b1b059b1f106be82b57f4a778da92882ab654f5d081ba6"},
- {file = "dbus_fast-1.38.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c364d32b5358d4ef42b16a605456634ce72753c270b9a2ff304ba81886570c99"},
- {file = "dbus_fast-1.38.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:78448e9b7c575257516a3d11941a0ec922a7d9d9fce9af8c3d62c3cc4a9515a6"},
- {file = "dbus_fast-1.38.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a31edd6101e18fc9aeae237374e97b5674f8802ce64d9930d5afa1db664e37d"},
- {file = "dbus_fast-1.38.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f5a61c89a27ccf621eaa801bc5d03117c7c886b2c66f4de801e8103607e07047"},
- {file = "dbus_fast-1.38.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ecb453b39219301b7f38ccc53a3c8fe20e94b8433fe510127ef74a113e1e43e"},
-]
+coverage = []
+cryptography = []
+dbus-fast = []
dill = [
{file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"},
{file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"},
]
-distlib = [
- {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
- {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
-]
-dodgy = [
- {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"},
- {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"},
-]
-execnet = [
- {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
- {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
-]
-filelock = [
- {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
- {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
-]
-flake8 = [
- {file = "flake8-2.3.0-py2.py3-none-any.whl", hash = "sha256:c99cc9716d6655d9c8bcb1e77632b8615bf0abd282d7abd9f5c2148cad7fc669"},
- {file = "flake8-2.3.0.tar.gz", hash = "sha256:5ee1a43ccd0716d6061521eec6937c983efa027793013e572712c4da55c7c83e"},
-]
-flake8-polyfill = [
- {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"},
- {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
-]
-fnvhash = [
- {file = "fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e"},
-]
-freezegun = [
- {file = "freezegun-1.2.1-py3-none-any.whl", hash = "sha256:15103a67dfa868ad809a8f508146e396be2995172d25f927e48ce51c0bf5cb09"},
- {file = "freezegun-1.2.1.tar.gz", hash = "sha256:b4c64efb275e6bc68dc6e771b17ffe0ff0f90b81a2a5189043550b6519926ba4"},
-]
-frozenlist = [
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
- {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
- {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
- {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
- {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
- {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
- {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
- {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
- {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
- {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
- {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
-]
+distlib = []
+dodgy = []
+execnet = []
+filelock = []
+flake8 = []
+flake8-polyfill = []
+fnvhash = []
+freezegun = []
+frozenlist = []
gitdb = [
{file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
-gitpython = [
- {file = "GitPython-3.1.28-py3-none-any.whl", hash = "sha256:77bfbd299d8709f6af7e0c70840ef26e7aff7cf0c1ed53b42dd7fc3a310fcb02"},
- {file = "GitPython-3.1.28.tar.gz", hash = "sha256:6bd3451b8271132f099ceeaf581392eaf6c274af74bb06144307870479d0697c"},
-]
-greenlet = [
- {file = "greenlet-1.1.3-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b"},
- {file = "greenlet-1.1.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32"},
- {file = "greenlet-1.1.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a"},
- {file = "greenlet-1.1.3-cp27-cp27m-win32.whl", hash = "sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318"},
- {file = "greenlet-1.1.3-cp27-cp27m-win_amd64.whl", hash = "sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49"},
- {file = "greenlet-1.1.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2"},
- {file = "greenlet-1.1.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e"},
- {file = "greenlet-1.1.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8"},
- {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477"},
- {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700"},
- {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26"},
- {file = "greenlet-1.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96"},
- {file = "greenlet-1.1.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7"},
- {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47"},
- {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2"},
- {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5"},
- {file = "greenlet-1.1.3-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa"},
- {file = "greenlet-1.1.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4"},
- {file = "greenlet-1.1.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76"},
- {file = "greenlet-1.1.3-cp35-cp35m-win32.whl", hash = "sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c"},
- {file = "greenlet-1.1.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20"},
- {file = "greenlet-1.1.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910"},
- {file = "greenlet-1.1.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d"},
- {file = "greenlet-1.1.3-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33"},
- {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57"},
- {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90"},
- {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e"},
- {file = "greenlet-1.1.3-cp36-cp36m-win32.whl", hash = "sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79"},
- {file = "greenlet-1.1.3-cp36-cp36m-win_amd64.whl", hash = "sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0"},
- {file = "greenlet-1.1.3-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3"},
- {file = "greenlet-1.1.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e"},
- {file = "greenlet-1.1.3-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba"},
- {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080"},
- {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41"},
- {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268"},
- {file = "greenlet-1.1.3-cp37-cp37m-win32.whl", hash = "sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc"},
- {file = "greenlet-1.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed"},
- {file = "greenlet-1.1.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08"},
- {file = "greenlet-1.1.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e"},
- {file = "greenlet-1.1.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25"},
- {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380"},
- {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809"},
- {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd"},
- {file = "greenlet-1.1.3-cp38-cp38-win32.whl", hash = "sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9"},
- {file = "greenlet-1.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202"},
- {file = "greenlet-1.1.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2"},
- {file = "greenlet-1.1.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe"},
- {file = "greenlet-1.1.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905"},
- {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382"},
- {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600"},
- {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403"},
- {file = "greenlet-1.1.3-cp39-cp39-win32.whl", hash = "sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1"},
- {file = "greenlet-1.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932"},
- {file = "greenlet-1.1.3.tar.gz", hash = "sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455"},
-]
+gitpython = []
+greenlet = []
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
]
-home-assistant-bluetooth = [
- {file = "home-assistant-bluetooth-1.3.0.tar.gz", hash = "sha256:da9fd3ba1e2231f69540f45d85636c2250a8fbe1ff74de216f9917a6afbe6d8b"},
- {file = "home_assistant_bluetooth-1.3.0-py3-none-any.whl", hash = "sha256:00aaffe0e5c37213d2f25b65aada15dd6d02f1b9d2fe89e927ac69e59ee8f2f9"},
-]
-homeassistant = [
- {file = "homeassistant-2022.10.2-py3-none-any.whl", hash = "sha256:4e58b78b415f1eb55cf0a2118731392c5ea197fb2111018d230e4221ee2f2773"},
- {file = "homeassistant-2022.10.2.tar.gz", hash = "sha256:ea3ea0c67ebf1c4481909996da2af6d7c5965bfc90600c42b89fc42cee48395d"},
-]
+home-assistant-bluetooth = []
+homeassistant = []
httpcore = [
{file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"},
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
@@ -2229,22 +1821,10 @@ httpx = [
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
]
-identify = [
- {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"},
- {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"},
-]
-idna = [
- {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
- {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
-]
-ifaddr = [
- {file = "ifaddr-0.1.7-py2.py3-none-any.whl", hash = "sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3"},
- {file = "ifaddr-0.1.7.tar.gz", hash = "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94"},
-]
-importlib-metadata = [
- {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
- {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
-]
+identify = []
+idna = []
+ifaddr = []
+importlib-metadata = []
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -2296,56 +1876,7 @@ lazy-object-proxy = [
{file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"},
{file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"},
]
-lru-dict = [
- {file = "lru-dict-1.1.8.tar.gz", hash = "sha256:878bc8ef4073e5cfb953dfc1cf4585db41e8b814c0106abde34d00ee0d0b3115"},
- {file = "lru_dict-1.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9d5815c0e85922cd0fb8344ca8b1c7cf020bf9fc45e670d34d51932c91fd7ec"},
- {file = "lru_dict-1.1.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f877f53249c3e49bbd7612f9083127290bede6c7d6501513567ab1bf9c581381"},
- {file = "lru_dict-1.1.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fef595c4f573141d54a38bda9221b9ee3cbe0acc73d67304a1a6d5972eb2a02"},
- {file = "lru_dict-1.1.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:db20597c4e67b4095b376ce2e83930c560f4ce481e8d05737885307ed02ba7c1"},
- {file = "lru_dict-1.1.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5b09dbe47bc4b4d45ffe56067aff190bc3c0049575da6e52127e114236e0a6a7"},
- {file = "lru_dict-1.1.8-cp310-cp310-win32.whl", hash = "sha256:3b1692755fef288b67af5cd8a973eb331d1f44cb02cbdc13660040809c2bfec6"},
- {file = "lru_dict-1.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:8f6561f9cd5a452cb84905c6a87aa944fdfdc0f41cc057d03b71f9b29b2cc4bd"},
- {file = "lru_dict-1.1.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ca8f89361e0e7aad0bf93ae03a31502e96280faeb7fb92267f4998fb230d36b2"},
- {file = "lru_dict-1.1.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c50ab9edaa5da5838426816a2b7bcde9d576b4fc50e6a8c062073dbc4969d78"},
- {file = "lru_dict-1.1.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fe16ade5fd0a57e9a335f69b8055aaa6fb278fbfa250458e4f6b8255115578f"},
- {file = "lru_dict-1.1.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:de972c7f4bc7b6002acff2a8de984c55fbd7f2289dba659cfd90f7a0f5d8f5d1"},
- {file = "lru_dict-1.1.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3d003a864899c29b0379e412709a6e516cbd6a72ee10b09d0b33226343617412"},
- {file = "lru_dict-1.1.8-cp36-cp36m-win32.whl", hash = "sha256:6e2a7aa9e36626fb48fdc341c7e3685a31a7b50ea4918677ea436271ad0d904d"},
- {file = "lru_dict-1.1.8-cp36-cp36m-win_amd64.whl", hash = "sha256:d2ed4151445c3f30423c2698f72197d64b27b1cd61d8d56702ffe235584e47c2"},
- {file = "lru_dict-1.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:075b9dd46d7022b675419bc6e3631748ae184bc8af195d20365a98b4f3bb2914"},
- {file = "lru_dict-1.1.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70364e3cbef536adab8762b4835e18f5ca8e3fddd8bd0ec9258c42bbebd0ee77"},
- {file = "lru_dict-1.1.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720f5728e537f11a311e8b720793a224e985d20e6b7c3d34a891a391865af1a2"},
- {file = "lru_dict-1.1.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c2fe692332c2f1d81fd27457db4b35143801475bfc2e57173a2403588dd82a42"},
- {file = "lru_dict-1.1.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:86d32a4498b74a75340497890a260d37bf1560ad2683969393032977dd36b088"},
- {file = "lru_dict-1.1.8-cp37-cp37m-win32.whl", hash = "sha256:348167f110494cfafae70c066470a6f4e4d43523933edf16ccdb8947f3b5fae0"},
- {file = "lru_dict-1.1.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9be6c4039ef328676b868acea619cd100e3de1a35b3be211cf0eaf9775563b65"},
- {file = "lru_dict-1.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a777d48319d293b1b6a933d606c0e4899690a139b4c81173451913bbcab6f44f"},
- {file = "lru_dict-1.1.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99f6cfb3e28490357a0805b409caf693e46c61f8dbb789c51355adb693c568d3"},
- {file = "lru_dict-1.1.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:163079dbda54c3e6422b23da39fb3ecc561035d65e8496ff1950cbdb376018e1"},
- {file = "lru_dict-1.1.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0972d669e9e207617e06416166718b073a49bf449abbd23940d9545c0847a4d9"},
- {file = "lru_dict-1.1.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97c24ffc55de6013075979f440acd174e88819f30387074639fb7d7178ca253e"},
- {file = "lru_dict-1.1.8-cp38-cp38-win32.whl", hash = "sha256:0f83cd70a6d32f9018d471be609f3af73058f700691657db4a3d3dd78d3f96dd"},
- {file = "lru_dict-1.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:add762163f4af7f4173fafa4092eb7c7f023cf139ef6d2015cfea867e1440d82"},
- {file = "lru_dict-1.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ac524e4615f06dc72ffbfd83f26e073c9ec256de5413634fbd024c010a8bc"},
- {file = "lru_dict-1.1.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7284bdbc5579bbdc3fc8f869ed4c169f403835566ab0f84567cdbfdd05241847"},
- {file = "lru_dict-1.1.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca497cb25f19f24171f9172805f3ff135b911aeb91960bd4af8e230421ccb51"},
- {file = "lru_dict-1.1.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1df1da204a9f0b5eb8393a46070f1d984fa8559435ee790d7f8f5602038fc00"},
- {file = "lru_dict-1.1.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f4d0a6d733a23865019b1c97ed6fb1fdb739be923192abf4dbb644f697a26a69"},
- {file = "lru_dict-1.1.8-cp39-cp39-win32.whl", hash = "sha256:7be1b66926277993cecdc174c15a20c8ce785c1f8b39aa560714a513eef06473"},
- {file = "lru_dict-1.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:881104711900af45967c2e5ce3e62291dd57d5b2a224d58b7c9f60bf4ad41b8c"},
- {file = "lru_dict-1.1.8-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:beb089c46bd95243d1ac5b2bd13627317b08bf40dd8dc16d4b7ee7ecb3cf65ca"},
- {file = "lru_dict-1.1.8-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10fe823ff90b655f0b6ba124e2b576ecda8c61b8ead76b456db67831942d22f2"},
- {file = "lru_dict-1.1.8-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07163c9dcbb2eca377f366b1331f46302fd8b6b72ab4d603087feca00044bb0"},
- {file = "lru_dict-1.1.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93336911544ebc0e466272043adab9fb9f6e9dcba6024b639c32553a3790e089"},
- {file = "lru_dict-1.1.8-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55aeda6b6789b2d030066b4f5f6fc3596560ba2a69028f35f3682a795701b5b1"},
- {file = "lru_dict-1.1.8-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:262a4e622010ceb960a6a5222ed011090e50954d45070fd369c0fa4d2ed7d9a9"},
- {file = "lru_dict-1.1.8-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6f64005ede008b7a866be8f3f6274dbf74e656e15e4004e9d99ad65efb01809"},
- {file = "lru_dict-1.1.8-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9d70257246b8207e8ef3d8b18457089f5ff0dfb087bd36eb33bce6584f2e0b3a"},
- {file = "lru_dict-1.1.8-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f874e9c2209dada1a080545331aa1277ec060a13f61684a8642788bf44b2325f"},
- {file = "lru_dict-1.1.8-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a592363c93d6fc6472d5affe2819e1c7590746aecb464774a4f67e09fbefdfc"},
- {file = "lru_dict-1.1.8-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f340b61f3cdfee71f66da7dbfd9a5ea2db6974502ccff2065cdb76619840dca"},
- {file = "lru_dict-1.1.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9447214e4857e16d14158794ef01e4501d8fad07d298d03308d9f90512df02fa"},
-]
+lru-dict = []
markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
@@ -2392,9 +1923,7 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
-mock-open = [
- {file = "mock-open-1.4.0.tar.gz", hash = "sha256:c3ecb6b8c32a5899a4f5bf4495083b598b520c698bba00e1ce2ace6e9c239100"},
-]
+mock-open = []
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
@@ -2456,32 +1985,7 @@ multidict = [
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
-mypy = [
- {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"},
- {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"},
- {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"},
- {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"},
- {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"},
- {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"},
- {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"},
- {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"},
- {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"},
- {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"},
- {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"},
- {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"},
- {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"},
- {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"},
- {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"},
- {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"},
- {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"},
- {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"},
- {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"},
- {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"},
- {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"},
- {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"},
- {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"},
- {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"},
-]
+mypy = []
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
@@ -2490,107 +1994,18 @@ nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
-numpy = [
- {file = "numpy-1.23.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5"},
- {file = "numpy-1.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8"},
- {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0"},
- {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5"},
- {file = "numpy-1.23.2-cp310-cp310-win32.whl", hash = "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450"},
- {file = "numpy-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414"},
- {file = "numpy-1.23.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c"},
- {file = "numpy-1.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524"},
- {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde"},
- {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418"},
- {file = "numpy-1.23.2-cp311-cp311-win32.whl", hash = "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722"},
- {file = "numpy-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e"},
- {file = "numpy-1.23.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f"},
- {file = "numpy-1.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842"},
- {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215"},
- {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66"},
- {file = "numpy-1.23.2-cp38-cp38-win32.whl", hash = "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d"},
- {file = "numpy-1.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d"},
- {file = "numpy-1.23.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0"},
- {file = "numpy-1.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6"},
- {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074"},
- {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77"},
- {file = "numpy-1.23.2-cp39-cp39-win32.whl", hash = "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c"},
- {file = "numpy-1.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913"},
- {file = "numpy-1.23.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38"},
- {file = "numpy-1.23.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f"},
- {file = "numpy-1.23.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389"},
- {file = "numpy-1.23.2.tar.gz", hash = "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01"},
-]
-orjson = [
- {file = "orjson-3.7.11-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:51e00a59dd6486c40f395da07633718f50b85af414e1add751f007dde6248090"},
- {file = "orjson-3.7.11-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c84d096f800d8cf062f8f514bb89baa1f067259ad8f71889b1d204039c2e2dd7"},
- {file = "orjson-3.7.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1afc49e56347e596653d3afd081ba30b353e6d2fe3499b71f118069cf13fcdbf"},
- {file = "orjson-3.7.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7051f5259aeef76492763a458d3d05efe820c0d20439aa3d3396b427fb40f85d"},
- {file = "orjson-3.7.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5b9ed454bf5237ad4bb0ec2170329a9a74dab065eaf2a2c31b84a7eff96c72"},
- {file = "orjson-3.7.11-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3f8c767331039e4e12324a6af41d3538c503503bdf107f40d4e292bb5542ff90"},
- {file = "orjson-3.7.11-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fd9508534ae29b368a60deb7668a65801869bc96635ee64550b7c119205984c0"},
- {file = "orjson-3.7.11-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7168059c4a02f3cbe2ce3a26908e199e38fe55feb325ee7484c61f15719ec85e"},
- {file = "orjson-3.7.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:87ab1b07ec863d870e8b2abcbae4da62aae2aed3a5119938a4b6309aa94ec973"},
- {file = "orjson-3.7.11-cp310-none-win_amd64.whl", hash = "sha256:01863ff99f67afdb1a3a6a777d2de5a81f9b8203db70ef450b25363e7db48442"},
- {file = "orjson-3.7.11-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a7962f2fb550a11f3e785c0aabfde6c2e7f823995f9d2d71f759708c6117a902"},
- {file = "orjson-3.7.11-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:4d33d13b0521ddca84b58c9a75c18e854b79480a6a13e6d0c105cfc0d4e8b2a7"},
- {file = "orjson-3.7.11-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b62b758b220f5deb6c90381baed8afec5d9b72e916886d73e944b78be3524f39"},
- {file = "orjson-3.7.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0639c5aeb75408b52ee60b19bff0aad299a12d31d6a68a8e9e86388f2d23d37"},
- {file = "orjson-3.7.11-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9390a69422ec12264bf76469c1cbd006a8672a552e7cc393664c66011343da71"},
- {file = "orjson-3.7.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a48e232130437fdbfc6c025cbf8aaac92c13ba1d9f7bd4445e177aae2f282028"},
- {file = "orjson-3.7.11-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:da1637f98a5e2ac6fe1a722f990474fbf05ca15a21f8bfbc2d06a14c62f74bfa"},
- {file = "orjson-3.7.11-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c2f52563dcb0c500f9c9a028459950e1d14b66f504f8e5cdb50122a2538b38b0"},
- {file = "orjson-3.7.11-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fbebb207a9d104efbd5e1b3e7dc3b63723ebbcd73f589f01bc7466b36c185e51"},
- {file = "orjson-3.7.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18026b1b1a0c78e277b07230e2713af79ec4b9a8225a778983fd2f8455ae0e09"},
- {file = "orjson-3.7.11-cp37-none-win_amd64.whl", hash = "sha256:77dff65c25dffea9e7dd9d41d3b55248dad2f6bf622d89e8ebb19a76780f9cd7"},
- {file = "orjson-3.7.11-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:f76e9d7a0c1a586999094bbfbed5c17246dc217ffea061356b7056d3805b31b8"},
- {file = "orjson-3.7.11-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0479adf8c7f18ba52ce30b64a03de2f1facb85b7a620832a0c8d5e01326f32bd"},
- {file = "orjson-3.7.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb97cba73ce2c474c380ca93350e261ab24fd955eac3a79389045adcc6199c1"},
- {file = "orjson-3.7.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:113add34e29ef4a0f8538d67dc4992a950a7b4f49e556525cd8247c82a3d3f6c"},
- {file = "orjson-3.7.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49cc542d3d2105fb7fb90a445ebe68f38cd846e6d86ea2c6e8724afbb9f052fc"},
- {file = "orjson-3.7.11-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:42e41ceda915e1602c0c8f5b00b0f852c8c0bb2f9262138e13bf02128de8a0b7"},
- {file = "orjson-3.7.11-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:df7f9b5d8c35e59c7df137587ebad2ec1d54947bbc6c7b1c4e7083c7012e3bba"},
- {file = "orjson-3.7.11-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17b77c2155da5186c18e3fe2ed5dc0d6babde5758fae81934a0a348c26430849"},
- {file = "orjson-3.7.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7fcbfc44a7fd94f55652e6705e03271c43b2a171220ee31d6447721b690acd9"},
- {file = "orjson-3.7.11-cp38-none-win_amd64.whl", hash = "sha256:78177a47c186cd6188e624477cbaf91c941a03047afe8b8816091495bc6481ce"},
- {file = "orjson-3.7.11-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:3a5324f0da7b15df64b6b586608af503c7fa8b0cfb6e2b9f4f4fdc4855af6978"},
- {file = "orjson-3.7.11-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:5c063c9777b5f795b9d59ba8d58b44548e3f2e9a00a9e3ddddb8145d9eb57b68"},
- {file = "orjson-3.7.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f77f3888f35d8aa28c12cc0355bb44fe29f9b631252cba4c7b5e4bb7a870778"},
- {file = "orjson-3.7.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf99de2f61fb8014a755640f9e2768890baf9aa1365742ccc3b9e6a19f528b16"},
- {file = "orjson-3.7.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71bc8155a08239a655d4cf821f106a0821d4eb566f7c7a0163ccc41763488116"},
- {file = "orjson-3.7.11-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:02a4875acb6e5f6109c40f7b9e27313bbe67f2c3e4d5ea01390ae9399061d913"},
- {file = "orjson-3.7.11-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6fc774923377e8594bf54291854919155e3c785081e95efc6cfcc9d76657a906"},
- {file = "orjson-3.7.11-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:01c77aab9ed881cc4322aca6ca3c534473f5334e5211b8dbb8622769595439ce"},
- {file = "orjson-3.7.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee69490145cc7d338a376a342415bba2f0c4d219f213c23fb64948cc40d9f255"},
- {file = "orjson-3.7.11-cp39-none-win_amd64.whl", hash = "sha256:145367654c236127f59894025a5354bce124bd6ee1d5417c28635969b7628482"},
- {file = "orjson-3.7.11.tar.gz", hash = "sha256:b4e6517861a397d9a1c72e7f8e8c72d6baf96d732a64637fb090ea49ead6042c"},
-]
+numpy = []
+orjson = []
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
-paho-mqtt = [
- {file = "paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"},
-]
-pathspec = [
- {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
- {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
-]
-pbr = [
- {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"},
- {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"},
-]
-pep8 = [
- {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"},
- {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"},
-]
-pep8-naming = [
- {file = "pep8-naming-0.10.0.tar.gz", hash = "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a"},
- {file = "pep8_naming-0.10.0-py2.py3-none-any.whl", hash = "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164"},
-]
-pipdeptree = [
- {file = "pipdeptree-2.3.1-py3-none-any.whl", hash = "sha256:fc0ffc6fd69f6f19e68bfd7722e5b4f66cc2b49a1be42ac7a3e8b4fd9f469370"},
- {file = "pipdeptree-2.3.1.tar.gz", hash = "sha256:30699521e1c5861b08d29d92398f67e9a5d7f613092257fff2a8bde3c948e05b"},
-]
+paho-mqtt = []
+pathspec = []
+pbr = []
+pep8 = []
+pep8-naming = []
+pipdeptree = []
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
@@ -2599,14 +2014,8 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
-pre-commit = [
- {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
- {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
-]
-prospector = [
- {file = "prospector-1.7.7-py3-none-any.whl", hash = "sha256:2dec5dac06f136880a3710996c0886dcc99e739007bbc05afc32884973f5c058"},
- {file = "prospector-1.7.7.tar.gz", hash = "sha256:c04b3d593e7c525cf9a742fed62afbe02e2874f0e42f2f56a49378fd94037360"},
-]
+pre-commit = []
+prospector = []
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@@ -2623,126 +2032,38 @@ pydocstyle = [
{file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
{file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
]
-pyflakes = [
- {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
- {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
-]
-pyjwt = [
- {file = "PyJWT-2.5.0-py3-none-any.whl", hash = "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80"},
- {file = "PyJWT-2.5.0.tar.gz", hash = "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b"},
-]
-pylint = [
- {file = "pylint-2.15.4-py3-none-any.whl", hash = "sha256:629cf1dbdfb6609d7e7a45815a8bb59300e34aa35783b5ac563acaca2c4022e9"},
- {file = "pylint-2.15.4.tar.gz", hash = "sha256:5441e9294335d354b7bad57c1044e5bd7cce25c433475d76b440e53452fa5cb8"},
-]
-pylint-celery = [
- {file = "pylint-celery-0.3.tar.gz", hash = "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"},
-]
-pylint-django = [
- {file = "pylint-django-2.5.3.tar.gz", hash = "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591"},
- {file = "pylint_django-2.5.3-py3-none-any.whl", hash = "sha256:56b12b6adf56d548412445bd35483034394a1a94901c3f8571980a13882299d5"},
-]
-pylint-flask = [
- {file = "pylint-flask-0.6.tar.gz", hash = "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"},
-]
-pylint-plugin-utils = [
- {file = "pylint-plugin-utils-0.7.tar.gz", hash = "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd"},
- {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"},
-]
-pyobjc-core = [
- {file = "pyobjc-core-8.5.1.tar.gz", hash = "sha256:f8592a12de076c27006700c4a46164478564fa33d7da41e7cbdd0a3bf9ddbccf"},
- {file = "pyobjc_core-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b62dcf987cc511188fc2aa5b4d3b9fd895361ea4984380463497ce4b0752ddf4"},
- {file = "pyobjc_core-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0accc653501a655f66c13f149a1d3d30e6cb65824edf852f7960a00c4f930d5b"},
- {file = "pyobjc_core-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f82b32affc898e9e5af041c1cecde2c99f2ce160b87df77f678c99f1550a4655"},
- {file = "pyobjc_core-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f7b2f6b6f3caeb882c658fe0c7098be2e8b79893d84daa8e636cb3e58a07df00"},
- {file = "pyobjc_core-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:872c0202c911a5a2f1269261c168e36569f6ddac17e5d854ac19e581726570cc"},
- {file = "pyobjc_core-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:21f92e231a4bae7f2d160d065f5afbf5e859a1e37f29d34ac12592205fc8c108"},
- {file = "pyobjc_core-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:315334dd09781129af6a39641248891c4caa57043901750b0139c6614ce84ec0"},
-]
-pyobjc-framework-cocoa = [
- {file = "pyobjc-framework-Cocoa-8.5.1.tar.gz", hash = "sha256:9a3de5cdb4644e85daf53f2ed912ef6c16ea5804a9e65552eafe62c2e139eb8c"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa572acc2628488a47be8d19f4701fc96fce7377cc4da18316e1e08c3918521a"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cb3ae21c8d81b7f02a891088c623cef61bca89bd671eff58c632d2f926b649f3"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:88f08f5bd94c66d373d8413c1d08218aff4cff0b586e0cc4249b2284023e7577"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:063683b57e4bd88cb0f9631ae65d25ec4eecf427d2fe8d0c578f88da9c896f3f"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f8806ddfac40620fb27f185d0f8937e69e330617319ecc2eccf6b9c8451bdd1"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7733a9a201df9e0cc2a0cf7bf54d76bd7981cba9b599353b243e3e0c9eefec10"},
- {file = "pyobjc_framework_Cocoa-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f0ab227f99d3e25dd3db73f8cde0999914a5f0dd6a08600349d25f95eaa0da63"},
-]
-pyobjc-framework-corebluetooth = [
- {file = "pyobjc-framework-CoreBluetooth-8.5.1.tar.gz", hash = "sha256:b4f621fc3b5bf289db58e64fd746773b18297f87a0ffc5502de74f69133301c1"},
- {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:bc720f2987a4d28dc73b13146e7c104d717100deb75c244da68f1d0849096661"},
- {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2167f22886beb5b3ae69e475e055403f28eab065c49a25e2b98b050b483be799"},
- {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:aa9587a36eca143701731e8bb6c369148f8cc48c28168d41e7323828e5117f2d"},
-]
-pyobjc-framework-libdispatch = [
- {file = "pyobjc-framework-libdispatch-8.5.1.tar.gz", hash = "sha256:066fb34fceb326307559104d45532ec2c7b55426f9910b70dbefd5d1b8fd530f"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a316646ab30ba2a97bc828f8e27e7bb79efdf993d218a9c5118396b4f81dc762"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7730a29e4d9c7d8c2e8d9ffb60af0ab6699b2186296d2bff0a2dd54527578bc3"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:76208d9d2b0071df2950800495ac0300360bb5f25cbe9ab880b65cb809764979"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ad9aa4773ff1d89bf4385c081824c4f8708b50e3ac2fe0a9d590153242c0f67"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:81e1833bd26f15930faba678f9efdffafc79ec04e2ea8b6d1b88cafc0883af97"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:73226e224436eb6383e7a8a811c90ed597995adb155b4f46d727881a383ac550"},
- {file = "pyobjc_framework_libdispatch-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d115355ce446fc073c75cedfd7ab0a13958adda8e3a3b1e421e1f1e5f65640da"},
-]
+pyflakes = []
+pyjwt = []
+pylint = []
+pylint-celery = []
+pylint-django = []
+pylint-flask = []
+pylint-plugin-utils = []
+pyobjc-core = []
+pyobjc-framework-cocoa = []
+pyobjc-framework-corebluetooth = []
+pyobjc-framework-libdispatch = []
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
-pytest = [
- {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
- {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
-]
-pytest-aiohttp = [
- {file = "pytest-aiohttp-0.3.0.tar.gz", hash = "sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f"},
- {file = "pytest_aiohttp-0.3.0-py3-none-any.whl", hash = "sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d"},
-]
+pytest = []
+pytest-aiohttp = []
pytest-cov = [
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
]
-pytest-forked = [
- {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"},
- {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"},
-]
-pytest-freezegun = [
- {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"},
- {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"},
-]
-pytest-homeassistant-custom-component = [
- {file = "pytest-homeassistant-custom-component-0.12.7.tar.gz", hash = "sha256:abce4cd50eddf6bf7cac3c03b5797ed956e24c89dfd0744a955dbcdc5c5244fd"},
- {file = "pytest_homeassistant_custom_component-0.12.7-py3-none-any.whl", hash = "sha256:ffd9315ac873bedf9f49357c0ee3de4e5c3eb0ac74f82ef8f2c0028b398eccaf"},
-]
-pytest-socket = [
- {file = "pytest-socket-0.5.1.tar.gz", hash = "sha256:7c4b81dc6a51cbc0093f11791de00ff4a15ac698f5da96879a80f5d9ad4179b6"},
- {file = "pytest_socket-0.5.1-py3-none-any.whl", hash = "sha256:8726fd47b83b127451532b6d570c5b6c4cd204fca363936509b1f53195de6f4f"},
-]
-pytest-sugar = [
- {file = "pytest-sugar-0.9.5.tar.gz", hash = "sha256:eea78b6f15b635277d3d90280cd386d8feea1cab0f9be75947a626e8b02b477d"},
- {file = "pytest_sugar-0.9.5-py2.py3-none-any.whl", hash = "sha256:3da42de32ce4e1e95b448d61c92804433f5d4058c0a765096991c2e93d5a289f"},
-]
-pytest-test-groups = [
- {file = "pytest-test-groups-1.0.3.tar.gz", hash = "sha256:a93ee8ae8605ad290965508d13efc975de64f80429465837af5f3dd5bc93fd96"},
-]
-pytest-timeout = [
- {file = "pytest-timeout-2.1.0.tar.gz", hash = "sha256:c07ca07404c612f8abbe22294b23c368e2e5104b521c1790195561f37e1ac3d9"},
- {file = "pytest_timeout-2.1.0-py3-none-any.whl", hash = "sha256:f6f50101443ce70ad325ceb4473c4255e9d74e3c7cd0ef827309dfa4c0d975c6"},
-]
-pytest-xdist = [
- {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"},
- {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"},
-]
-python-dateutil = [
- {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
- {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
-]
-python-slugify = [
- {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"},
-]
-pytz = [
- {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"},
- {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"},
-]
+pytest-forked = []
+pytest-freezegun = []
+pytest-homeassistant-custom-component = []
+pytest-socket = []
+pytest-sugar = []
+pytest-test-groups = []
+pytest-timeout = []
+pytest-xdist = []
+python-dateutil = []
+python-slugify = []
+pytz = []
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
@@ -2751,13 +2072,6 @@ pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
- {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
- {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
- {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
- {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
- {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
- {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
- {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -2785,29 +2099,15 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
-requests = [
- {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
- {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
-]
-requests-mock = [
- {file = "requests-mock-1.10.0.tar.gz", hash = "sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"},
- {file = "requests_mock-1.10.0-py2.py3-none-any.whl", hash = "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699"},
-]
-requirements-detector = [
- {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"},
-]
-respx = [
- {file = "respx-0.19.2-py2.py3-none-any.whl", hash = "sha256:417f986fec599b9cc6531e93e494b7a75d1cb7bccff9dde5b53edc51f7954494"},
- {file = "respx-0.19.2.tar.gz", hash = "sha256:f3d210bb4de0ccc4c5afabeb87c3c1b03b3765a9c1a73eb042a07bb18ac33705"},
-]
+requests = []
+requests-mock = []
+requirements-detector = []
+respx = []
rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
-setoptconf-tmp = [
- {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"},
- {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"},
-]
+setoptconf-tmp = []
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -2816,10 +2116,7 @@ smmap = [
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
-sniffio = [
- {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
- {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
-]
+sniffio = []
snowballstemmer = [
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
@@ -2828,69 +2125,12 @@ soupsieve = [
{file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
]
-sqlalchemy = [
- {file = "SQLAlchemy-1.4.41-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d"},
- {file = "SQLAlchemy-1.4.41-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330"},
- {file = "SQLAlchemy-1.4.41-cp27-cp27m-win32.whl", hash = "sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d"},
- {file = "SQLAlchemy-1.4.41-cp27-cp27m-win_amd64.whl", hash = "sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05"},
- {file = "SQLAlchemy-1.4.41-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-win32.whl", hash = "sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd"},
- {file = "SQLAlchemy-1.4.41-cp310-cp310-win_amd64.whl", hash = "sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251"},
- {file = "SQLAlchemy-1.4.41-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536"},
- {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb"},
- {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded"},
- {file = "SQLAlchemy-1.4.41-cp311-cp311-win32.whl", hash = "sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0"},
- {file = "SQLAlchemy-1.4.41-cp311-cp311-win_amd64.whl", hash = "sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-win32.whl", hash = "sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682"},
- {file = "SQLAlchemy-1.4.41-cp36-cp36m-win_amd64.whl", hash = "sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-win32.whl", hash = "sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767"},
- {file = "SQLAlchemy-1.4.41-cp37-cp37m-win_amd64.whl", hash = "sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-win32.whl", hash = "sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a"},
- {file = "SQLAlchemy-1.4.41-cp38-cp38-win_amd64.whl", hash = "sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-win32.whl", hash = "sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d"},
- {file = "SQLAlchemy-1.4.41-cp39-cp39-win_amd64.whl", hash = "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"},
- {file = "SQLAlchemy-1.4.41.tar.gz", hash = "sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791"},
-]
-stdlib-list = [
- {file = "stdlib-list-0.7.0.tar.gz", hash = "sha256:66c1c1724a12667cdb35be9f43181c3e6646c194e631efaaa93c1f2c2c7a1f7f"},
- {file = "stdlib_list-0.7.0-py3-none-any.whl", hash = "sha256:0ed79a0badf4f666aad046cde364ccac68ca1438a211ec74b0153e0eb5642a3e"},
-]
-stevedore = [
- {file = "stevedore-4.0.0-py3-none-any.whl", hash = "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8"},
- {file = "stevedore-4.0.0.tar.gz", hash = "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786"},
-]
-termcolor = [
- {file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"},
- {file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"},
-]
-teslajsonpy = [
- {file = "teslajsonpy-2.4.5-py3-none-any.whl", hash = "sha256:36f98a5c15fb68874aeb9f40f7eb31993035c69b541e3a7b0a557a9ebb0d7d4a"},
- {file = "teslajsonpy-2.4.5.tar.gz", hash = "sha256:bbeb6e9dd276381425fcd75366e679e7892705c5df4b89593450f1a1286d8c48"},
-]
-text-unidecode = [
- {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
- {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
-]
+sqlalchemy = []
+stdlib-list = []
+stevedore = []
+termcolor = []
+teslajsonpy = []
+text-unidecode = []
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -2899,38 +2139,17 @@ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
-tomlkit = [
- {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"},
- {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"},
-]
+tomlkit = []
tqdm = [
{file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"},
{file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"},
]
-typer = [
- {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"},
- {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"},
-]
-typing-extensions = [
- {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
- {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
-]
-urllib3 = [
- {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
- {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
-]
-virtualenv = [
- {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
- {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
-]
-voluptuous = [
- {file = "voluptuous-0.13.1-py3-none-any.whl", hash = "sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6"},
- {file = "voluptuous-0.13.1.tar.gz", hash = "sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723"},
-]
-voluptuous-serialize = [
- {file = "voluptuous-serialize-2.5.0.tar.gz", hash = "sha256:5359f2e0a4f972ae03066e0777b4f0755c9226b2af099ca4fc55432efd1a447b"},
- {file = "voluptuous_serialize-2.5.0-py3-none-any.whl", hash = "sha256:bec8bc2d48c636692f9ab9caf7efda3951183a30bfe96d1ffe87369c96d9beca"},
-]
+typer = []
+typing-extensions = []
+urllib3 = []
+virtualenv = []
+voluptuous = []
+voluptuous-serialize = []
wrapt = [
{file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
@@ -2997,68 +2216,5 @@ wrapt = [
{file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
{file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
]
-yarl = [
- {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
- {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
- {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
- {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
- {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
- {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
- {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
- {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
- {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
- {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
- {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
- {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
- {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
-]
-zipp = [
- {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"},
- {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"},
-]
+yarl = []
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
index 1b54d4af..e59d01cd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,11 +6,11 @@ authors = ["Alan D. Tse "]
license = "Apache-2.0"
[tool.poetry.dependencies]
-python = "^3.9"
-teslajsonpy = "^2.4.5"
+python = "^3.10"
+teslajsonpy = "^3.0.0"
[tool.poetry.dev-dependencies]
-homeassistant = ">=2021.3.4"
+homeassistant = ">=2021.10.0"
pytest-homeassistant-custom-component = ">=0.3.1"
bandit = ">=1.7.0"
black = {version = ">=21.12b0", allow-prereleases = true}
@@ -36,7 +36,7 @@ commit_subject="[skip ci] {version}"
[tool.black]
line-length = 88
-target-version = ['py38']
+target-version = ['py310']
exclude = '''
(
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 00000000..0ddd5672
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,91 @@
+"""Common methods used across tests for Tesla."""
+from datetime import datetime
+from unittest.mock import patch
+
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_DOMAIN,
+ CONF_TOKEN,
+ CONF_USERNAME,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from teslajsonpy.car import TeslaCar
+from teslajsonpy.energy import SolarSite, SolarPowerwallSite
+from teslajsonpy.const import AUTH_DOMAIN
+
+from custom_components.tesla_custom.const import CONF_EXPIRATION, DOMAIN as TESLA_DOMIN
+
+from .const import TEST_ACCESS_TOKEN, TEST_TOKEN, TEST_USERNAME, TEST_VALID_EXPIRATION
+from .mock_data import car as car_mock_data
+from .mock_data import energysite as energysite_mock_data
+
+
+def setup_mock_controller(mock_controller):
+ """Setup a mock controller with mock data."""
+
+ instance = mock_controller.return_value
+
+ instance.is_car_online.return_value = True
+ instance.get_last_update_time.return_value = datetime.now()
+ instance.get_last_update_time.return_value = datetime.now()
+ instance.update_interval.return_value = 660
+
+ instance.get_tokens.return_value = {
+ "refresh_token": TEST_TOKEN,
+ "access_token": TEST_ACCESS_TOKEN,
+ "expiration": TEST_VALID_EXPIRATION,
+ }
+
+ instance.generate_car_objects.return_value = {
+ car_mock_data.VIN: TeslaCar(
+ car_mock_data.VEHICLE,
+ mock_controller.return_value,
+ car_mock_data.VEHICLE_DATA,
+ )
+ }
+
+ instance.generate_energysite_objects.return_value = {
+ 12345: SolarSite(
+ mock_controller.api,
+ energysite_mock_data.ENERGYSITE_SOLAR,
+ energysite_mock_data.SITE_CONFIG_SOLAR,
+ energysite_mock_data.SITE_DATA,
+ ),
+ 67890: SolarPowerwallSite(
+ mock_controller.api,
+ energysite_mock_data.ENERGYSITE_BATTERY,
+ energysite_mock_data.SITE_CONFIG_POWERWALL,
+ energysite_mock_data.BATTERY_DATA,
+ energysite_mock_data.BATTERY_SUMMARY,
+ ),
+ }
+
+
+async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry:
+ """Set up the Tesla platform."""
+
+ mock_entry = MockConfigEntry(
+ domain=TESLA_DOMIN,
+ title=TEST_USERNAME,
+ data={
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
+ CONF_TOKEN: TEST_TOKEN,
+ CONF_EXPIRATION: TEST_VALID_EXPIRATION,
+ CONF_DOMAIN: AUTH_DOMAIN,
+ },
+ options=None,
+ )
+
+ mock_entry.add_to_hass(hass)
+
+ with patch("custom_components.tesla_custom.PLATFORMS", [platform]), patch(
+ "custom_components.tesla_custom.TeslaAPI", autospec=True
+ ) as mock_controller:
+ setup_mock_controller(mock_controller)
+ assert await async_setup_component(hass, TESLA_DOMIN, {})
+ await hass.async_block_till_done()
+
+ return mock_entry, mock_controller
diff --git a/tests/const.py b/tests/const.py
new file mode 100644
index 00000000..df60d7c6
--- /dev/null
+++ b/tests/const.py
@@ -0,0 +1,7 @@
+import datetime
+
+TEST_USERNAME = "test-username"
+TEST_TOKEN = "test-token"
+TEST_PASSWORD = "test-password"
+TEST_ACCESS_TOKEN = "test-access-token"
+TEST_VALID_EXPIRATION = datetime.datetime.now().timestamp() * 2
diff --git a/tests/mock_data/__init__.py b/tests/mock_data/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/mock_data/car.py b/tests/mock_data/car.py
new file mode 100644
index 00000000..9cf723e5
--- /dev/null
+++ b/tests/mock_data/car.py
@@ -0,0 +1,264 @@
+RESULT_OK = {"response": {"reason": "", "result": True}}
+RESULT_NOT_OK = {"response": {"reason": "", "result": False}}
+
+# 408 - Request Timeout
+RESULT_VEHICLE_UNAVAILABLE = {
+ "response": None,
+ "error": 'vehicle unavailable: {:error=>"vehicle unavailable:"}',
+ "error_description": "",
+}
+
+
+VIN = "5yjsa11111111111"
+CAR_ID = 12345678901234567
+# 2015 Model S 85D with some manually added keys (sentry mode, heated steering) for testing
+VEHICLE_DATA = {
+ "id": 12345678901234567,
+ "user_id": 123456,
+ "vehicle_id": 1234567890,
+ "vin": "5YJSA11111111111",
+ "display_name": "My Model S",
+ "option_codes": "AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0",
+ "color": None,
+ "access_type": "OWNER",
+ "tokens": ["redacted", "redacted"],
+ "state": "online",
+ "in_service": False,
+ "id_s": "12345678901234567",
+ "calendar_enabled": True,
+ "api_version": 36,
+ "backseat_token": None,
+ "backseat_token_updated_at": None,
+ "charge_state": {
+ "battery_heater_on": False,
+ "battery_level": 78,
+ "battery_range": 169.08,
+ "charge_amps": 32,
+ "charge_current_request": 32,
+ "charge_current_request_max": 32,
+ "charge_enable_request": True,
+ "charge_energy_added": 13.57,
+ "charge_limit_soc": 80,
+ "charge_limit_soc_max": 100,
+ "charge_limit_soc_min": 50,
+ "charge_limit_soc_std": 90,
+ "charge_miles_added_ideal": 59.0,
+ "charge_miles_added_rated": 47.0,
+ "charge_port_cold_weather_mode": None,
+ "charge_port_color": "FlashingGreen",
+ "charge_port_door_open": True,
+ "charge_port_latch": "Engaged",
+ "charge_rate": 23.2,
+ "charge_to_max_range": False,
+ "charger_actual_current": 32,
+ "charger_phases": 1,
+ "charger_pilot_current": 32,
+ "charger_power": 7,
+ "charger_voltage": 242,
+ "charging_state": "Charging",
+ "conn_charge_cable": "SAE",
+ "est_battery_range": 150.09,
+ "fast_charger_brand": "",
+ "fast_charger_present": False,
+ "fast_charger_type": "MCSingleWireCAN",
+ "ideal_battery_range": 213.19,
+ "managed_charging_active": False,
+ "managed_charging_start_time": None,
+ "managed_charging_user_canceled": False,
+ "max_range_charge_counter": 0,
+ "minutes_to_full_charge": 15,
+ "not_enough_power_to_heat": False,
+ "off_peak_charging_enabled": True,
+ "off_peak_charging_times": "weekdays",
+ "off_peak_hours_end_time": 360,
+ "preconditioning_enabled": False,
+ "preconditioning_times": "all_week",
+ "scheduled_charging_mode": "DepartBy",
+ "scheduled_charging_pending": False,
+ "scheduled_charging_start_time": None,
+ "scheduled_charging_start_time_app": 0,
+ "scheduled_departure_time": 1661515200,
+ "scheduled_departure_time_minutes": 300,
+ "supercharger_session_trip_planner": False,
+ "time_to_full_charge": 0.25,
+ "timestamp": 1661641175268,
+ "trip_charging": False,
+ "usable_battery_level": 78,
+ "user_charge_enable_request": None,
+ },
+ "climate_state": {
+ "allow_cabin_overheat_protection": True,
+ "battery_heater": False,
+ "battery_heater_no_power": False,
+ "cabin_overheat_protection": "Off",
+ "climate_keeper_mode": "off",
+ "defrost_mode": 0,
+ "driver_temp_setting": 23.3,
+ "fan_status": 0,
+ "hvac_auto_request": "On",
+ "inside_temp": 35.5,
+ "is_auto_conditioning_on": False,
+ "is_climate_on": False,
+ "is_front_defroster_on": False,
+ "is_preconditioning": False,
+ "is_rear_defroster_on": False,
+ "left_temp_direction": -309,
+ "max_avail_temp": 28.0,
+ "min_avail_temp": 15.0,
+ "outside_temp": 32.5,
+ "passenger_temp_setting": 23.3,
+ "remote_heater_control_enabled": False,
+ "right_temp_direction": -309,
+ "seat_heater_left": 0,
+ "seat_heater_right": 0,
+ "side_mirror_heaters": False,
+ "supports_fan_only_cabin_overheat_protection": False,
+ "timestamp": 1661641175268,
+ "wiper_blade_heater": False,
+ "steering_wheel_heater": True,
+ },
+ "drive_state": {
+ "gps_as_of": 1661641173,
+ "heading": 182,
+ "latitude": 33.111111,
+ "longitude": -88.111111,
+ "native_latitude": 33.111111,
+ "native_location_supported": 1,
+ "native_longitude": -88.111111,
+ "native_type": "wgs",
+ "power": -7,
+ "shift_state": None,
+ "speed": None,
+ "timestamp": 1661641175268,
+ },
+ "gui_settings": {
+ "gui_24_hour_time": False,
+ "gui_charge_rate_units": "mi/hr",
+ "gui_distance_units": "mi/hr",
+ "gui_range_display": "Rated",
+ "gui_temperature_units": "F",
+ "show_range_units": True,
+ "timestamp": 1661641175268,
+ },
+ "vehicle_config": {
+ "can_accept_navigation_requests": True,
+ "can_actuate_trunks": True,
+ "car_special_type": "base",
+ "car_type": "models",
+ "charge_port_type": "US",
+ "dashcam_clip_save_supported": False,
+ "default_charge_to_max": False,
+ "driver_assist": "MonoCam",
+ "ece_restrictions": False,
+ "efficiency_package": "Default",
+ "eu_vehicle": False,
+ "exterior_color": "White",
+ "front_drive_unit": "NoneOrSmall",
+ "has_air_suspension": False,
+ "has_ludicrous_mode": False,
+ "has_seat_cooling": False,
+ "headlamp_type": "Hid",
+ "interior_trim_type": "AllBlack",
+ "motorized_charge_port": True,
+ "plg": True,
+ "pws": False,
+ "rear_drive_unit": "Small",
+ "rear_seat_heaters": 0,
+ "rear_seat_type": 1,
+ "rhd": False,
+ "roof_color": "Colored",
+ "seat_type": 1,
+ "spoiler_type": "None",
+ "sun_roof_installed": 0,
+ "third_row_seats": "None",
+ "timestamp": 1661641175269,
+ "trim_badging": "85d",
+ "use_range_badging": False,
+ "utc_offset": -25200,
+ "wheel_type": "Base19",
+ },
+ "vehicle_state": {
+ "api_version": 36,
+ "autopark_state_v2": "standby",
+ "autopark_style": "standard",
+ "calendar_supported": True,
+ "car_version": "2022.8.10.1 171f0fe61c20",
+ "center_display_state": 0,
+ "dashcam_clip_save_available": False,
+ "dashcam_state": "",
+ "df": 0,
+ "dr": 0,
+ "fd_window": 0,
+ "feature_bitmask": "5,0",
+ "fp_window": 0,
+ "ft": 0,
+ "homelink_device_count": 2,
+ "homelink_nearby": True,
+ "is_user_present": False,
+ "last_autopark_error": "no_error",
+ "locked": False,
+ "media_state": {"remote_control_enabled": True},
+ "notifications_supported": True,
+ "odometer": 70915.596752,
+ "parsed_calendar_supported": True,
+ "pf": 0,
+ "pr": 0,
+ "rd_window": 0,
+ "remote_start": False,
+ "remote_start_enabled": True,
+ "remote_start_supported": True,
+ "rp_window": 0,
+ "rt": 0,
+ "santa_mode": 0,
+ "smart_summon_available": False,
+ "software_update": {
+ "download_perc": 0,
+ "expected_duration_sec": 2700,
+ "install_perc": 1,
+ "status": "",
+ "version": "2022.8.10.1",
+ },
+ "speed_limit_mode": {
+ "active": False,
+ "current_limit_mph": 85.0,
+ "max_limit_mph": 90,
+ "min_limit_mph": 50.0,
+ "pin_code_set": False,
+ },
+ "summon_standby_mode_enabled": False,
+ "timestamp": 1661641175268,
+ "tpms_pressure_fl": None,
+ "tpms_pressure_fr": None,
+ "tpms_pressure_rl": None,
+ "tpms_pressure_rr": None,
+ "valet_mode": False,
+ "valet_pin_needed": True,
+ "vehicle_name": "My Model S",
+ "sentry_mode": True,
+ "sentry_mode_available": True,
+ },
+}
+
+VEHICLE = {
+ "id": 12345678901234567,
+ "user_id": 123,
+ "vehicle_id": 1234567890,
+ "vin": "5YJSA11111111111",
+ "display_name": "My Model S",
+ "option_codes": "MDLS,RENA,AF02,APF1,APH2,APPB,AU01,BC0R,BP00,BR00,BS00,CDM0,CH05,PBCW,CW00,DCF0,DRLH,DSH7,DV4W,FG02,FR04,HP00,IDBA,IX01,LP01,ME02,MI01,PF01,PI01,PK00,PS01,PX00,PX4D,QTVB,RFP2,SC01,SP00,SR01,SU01,TM00,TP03,TR00,UTAB,WTAS,X001,X003,X007,X011,X013,X021,X024,X027,X028,X031,X037,X040,X044,YFFC,COUS",
+ "color": None,
+ "tokens": ["abcdef1234567890", "1234567890abcdef"],
+ "state": "online",
+ "in_service": False,
+ "id_s": "12345678901234567",
+ "calendar_enabled": True,
+ "api_version": 7,
+ "backseat_token": None,
+ "backseat_token_updated_at": None,
+ "drive_state": None,
+ "climate_state": None,
+ "charge_state": None,
+ "gui_settings": None,
+ "vehicle_state": None,
+ "vehicle_config": None,
+}
diff --git a/tests/mock_data/energysite.py b/tests/mock_data/energysite.py
new file mode 100644
index 00000000..3840ab13
--- /dev/null
+++ b/tests/mock_data/energysite.py
@@ -0,0 +1,500 @@
+ENERGYSITE_SOLAR = {
+ "energy_site_id": 12345,
+ "resource_type": "solar",
+ "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac",
+ "asset_site_id": "12345",
+ "solar_power": 2260,
+ "solar_type": "pv_panel",
+ "storm_mode_enabled": None,
+ "powerwall_onboarding_settings_set": None,
+ "sync_grid_alert_enabled": False,
+ "breaker_alert_enabled": False,
+ "components": {
+ "battery": False,
+ "solar": True,
+ "solar_type": "pv_panel",
+ "grid": True,
+ "load_meter": True,
+ "market_type": "residential",
+ },
+}
+
+ENERGYSITE_BATTERY = {
+ "energy_site_id": 67890,
+ "resource_type": "battery",
+ "site_name": "Battery Home",
+ "id": "XXX",
+ "asset_site_id": "67890",
+ "solar_power": 3456,
+ "solar_type": "pv_panel",
+ "storm_mode_enabled": None,
+ "powerwall_onboarding_settings_set": None,
+ "sync_grid_alert_enabled": False,
+ "breaker_alert_enabled": False,
+ "components": {
+ "battery": True,
+ "solar": True,
+ "solar_type": "pv_panel",
+ "grid": True,
+ "load_meter": True,
+ "market_type": "residential",
+ },
+}
+
+SITE_CONFIG_SOLAR = {
+ "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac",
+ "site_name": "My Home",
+ "site_number": "2252147638651575",
+ "installation_date": "2022-04-04T15:56:35-07:00",
+ "user_settings": {
+ "storm_mode_enabled": None,
+ "powerwall_onboarding_settings_set": None,
+ "sync_grid_alert_enabled": False,
+ "breaker_alert_enabled": False,
+ },
+ "components": {
+ "solar": True,
+ "solar_type": "pv_panel",
+ "battery": False,
+ "grid": True,
+ "backup": False,
+ "gateway": "gateway_type_none",
+ "load_meter": True,
+ "tou_capable": False,
+ "storm_mode_capable": False,
+ "flex_energy_request_capable": False,
+ "car_charging_data_supported": False,
+ "off_grid_vehicle_charging_reserve_supported": False,
+ "vehicle_charging_performance_view_enabled": False,
+ "vehicle_charging_solar_offset_view_enabled": False,
+ "battery_solar_offset_view_enabled": False,
+ "energy_service_self_scheduling_enabled": True,
+ "rate_plan_manager_supported": True,
+ "configurable": False,
+ "grid_services_enabled": False,
+ },
+ "installation_time_zone": "America/Los_Angeles",
+ "time_zone_offset": -420,
+ "geolocation": {"latitude": 32.53452700000001, "longitude": -112.3463137},
+ "address": {
+ "address_line1": "1234 Tesla Solar Ave",
+ "city": "Austin",
+ "state": "TX",
+ "zip": "123456",
+ "country": "US",
+ },
+}
+
+SITE_CONFIG_POWERWALL = {
+ "id": "XXX",
+ "site_name": "Battery Home",
+ "backup_reserve_percent": 0,
+ "default_real_mode": "self_consumption",
+ "installation_date": "2022-03-21T17:15:23+10:00",
+ "user_settings": {
+ "storm_mode_enabled": True,
+ "powerwall_onboarding_settings_set": True,
+ "sync_grid_alert_enabled": True,
+ "breaker_alert_enabled": False,
+ },
+ "components": {
+ "solar": True,
+ "solar_type": "pv_panel",
+ "battery": True,
+ "grid": True,
+ "backup": True,
+ "gateway": "teg",
+ "load_meter": True,
+ "tou_capable": True,
+ "storm_mode_capable": True,
+ "flex_energy_request_capable": False,
+ "car_charging_data_supported": False,
+ "off_grid_vehicle_charging_reserve_supported": False,
+ "vehicle_charging_performance_view_enabled": False,
+ "vehicle_charging_solar_offset_view_enabled": False,
+ "battery_solar_offset_view_enabled": True,
+ "solar_value_enabled": True,
+ "energy_value_header": "Energy Value",
+ "energy_value_subheader": "Estimated Value",
+ "energy_service_self_scheduling_enabled": True,
+ "show_grid_import_battery_source_cards": True,
+ "set_islanding_mode_enabled": True,
+ "wifi_commissioning_enabled": True,
+ "backup_time_remaining_enabled": True,
+ "rate_plan_manager_supported": True,
+ "battery_type": "ac_powerwall",
+ "configurable": True,
+ "grid_services_enabled": False,
+ "customer_preferred_export_rule": "battery_ok",
+ "net_meter_mode": "battery_ok",
+ "edit_setting_permission_to_export": True,
+ "edit_setting_grid_charging": True,
+ "edit_setting_energy_exports": True,
+ },
+ "version": "22.18.3 21c0ad81",
+ "battery_count": 1,
+ "tariff_content": {
+ "name": "Wholesale",
+ "utility": "Amber",
+ "daily_charges": [{"amount": 0, "name": "Charge"}],
+ "demand_charges": {"ALL": {"ALL": 0}, "Summer": {}, "Winter": {}},
+ "energy_charges": {
+ "ALL": {"ALL": 0},
+ "Summer": {"ON_PEAK": 0.44, "PARTIAL_PEAK": 0.3},
+ "Winter": {},
+ },
+ "seasons": {
+ "Summer": {
+ "fromDay": 1,
+ "toDay": 31,
+ "fromMonth": 1,
+ "toMonth": 12,
+ "tou_periods": {
+ "ON_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 3,
+ "fromMinute": 0,
+ "toHour": 5,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 6,
+ "fromMinute": 0,
+ "toHour": 7,
+ "toMinute": 30,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 16,
+ "fromMinute": 0,
+ "toHour": 21,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 23,
+ "fromMinute": 0,
+ "toHour": 1,
+ "toMinute": 0,
+ },
+ ],
+ "PARTIAL_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 7,
+ "fromMinute": 30,
+ "toHour": 10,
+ "toMinute": 30,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 14,
+ "fromMinute": 0,
+ "toHour": 16,
+ "toMinute": 0,
+ },
+ ],
+ "OFF_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 1,
+ "fromMinute": 0,
+ "toHour": 3,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 5,
+ "fromMinute": 0,
+ "toHour": 6,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 10,
+ "fromMinute": 30,
+ "toHour": 14,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 21,
+ "fromMinute": 0,
+ "toHour": 23,
+ "toMinute": 0,
+ },
+ ],
+ },
+ },
+ "Winter": {
+ "fromDay": 0,
+ "toDay": 0,
+ "fromMonth": 0,
+ "toMonth": 0,
+ "tou_periods": {},
+ },
+ },
+ "sell_tariff": {
+ "name": "Wholesale",
+ "utility": "Amber",
+ "daily_charges": [{"amount": 0, "name": "Charge"}],
+ "demand_charges": {"ALL": {"ALL": 0}, "Summer": {}, "Winter": {}},
+ "energy_charges": {
+ "ALL": {"ALL": 0},
+ "Summer": {"ON_PEAK": 0.32, "PARTIAL_PEAK": 0.04},
+ "Winter": {},
+ },
+ "seasons": {
+ "Summer": {
+ "fromDay": 1,
+ "toDay": 31,
+ "fromMonth": 1,
+ "toMonth": 12,
+ "tou_periods": {
+ "ON_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 3,
+ "fromMinute": 0,
+ "toHour": 5,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 6,
+ "fromMinute": 0,
+ "toHour": 7,
+ "toMinute": 30,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 16,
+ "fromMinute": 0,
+ "toHour": 21,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 23,
+ "fromMinute": 0,
+ "toHour": 1,
+ "toMinute": 0,
+ },
+ ],
+ "PARTIAL_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 7,
+ "fromMinute": 30,
+ "toHour": 10,
+ "toMinute": 30,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 14,
+ "fromMinute": 0,
+ "toHour": 16,
+ "toMinute": 0,
+ },
+ ],
+ "OFF_PEAK": [
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 1,
+ "fromMinute": 0,
+ "toHour": 3,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 5,
+ "fromMinute": 0,
+ "toHour": 6,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 10,
+ "fromMinute": 30,
+ "toHour": 14,
+ "toMinute": 0,
+ },
+ {
+ "fromDayOfWeek": 0,
+ "toDayOfWeek": 6,
+ "fromHour": 21,
+ "fromMinute": 0,
+ "toHour": 23,
+ "toMinute": 0,
+ },
+ ],
+ },
+ },
+ "Winter": {
+ "fromDay": 0,
+ "toDay": 0,
+ "fromMonth": 0,
+ "toMonth": 0,
+ "tou_periods": {},
+ },
+ },
+ },
+ },
+ "nameplate_power": 5000,
+ "nameplate_energy": 13500,
+ "installation_time_zone": "Australia/Brisbane",
+ "max_site_meter_power_ac": 1000000000,
+ "min_site_meter_power_ac": -1000000000,
+ "geolocation": {"latitude": -123.456, "longitude": 123.456},
+ "address": {
+ "address_line1": "XXX",
+ "city": "YYYY",
+ "state": "QLD",
+ "zip": "NNN",
+ "country": "AU",
+ },
+}
+
+SITE_DATA = {
+ "solar_power": 7720,
+ "energy_left": 0,
+ "total_pack_energy": 1,
+ "percentage_charged": 0,
+ "battery_power": 0,
+ "load_power": 4517.14990234375,
+ "grid_status": "Unknown",
+ "grid_services_active": False,
+ "grid_power": -3202.85009765625,
+ "grid_services_power": 0,
+ "generator_power": 0,
+ "island_status": "island_status_unknown",
+ "storm_mode_active": False,
+ "timestamp": "2022-07-28T17:11:27Z",
+ "wall_connectors": None,
+}
+
+BATTERY_DATA = {
+ "site_name": "Battery Home",
+ "energy_left": 0,
+ "total_pack_energy": 1,
+ "grid_status": "Active",
+ "backup": {
+ "backup_reserve_percent": 0,
+ "events": [
+ {"timestamp": "2022-07-12T06:56:55+10:00", "duration": 38773},
+ {"timestamp": "2022-07-11T20:46:25+10:00", "duration": 66479},
+ {"timestamp": "2022-06-29T11:35:43+10:00", "duration": 842030},
+ {"timestamp": "2022-06-18T15:28:35+10:00", "duration": 1013486},
+ {"timestamp": "2022-06-15T15:43:20+10:00", "duration": 210737},
+ {"timestamp": "2022-06-10T08:26:12+10:00", "duration": 47649},
+ {"timestamp": "2022-06-03T13:58:52+10:00", "duration": 443079},
+ {"timestamp": "2022-05-15T10:46:58+10:00", "duration": 31389950},
+ {"timestamp": "2022-05-14T15:33:38+10:00", "duration": 1279604},
+ {"timestamp": "2022-05-07T19:39:07+10:00", "duration": 901817},
+ {"timestamp": "2022-04-23T08:26:14+10:00", "duration": 437693},
+ {"timestamp": "2022-04-22T19:14:33+10:00", "duration": 757615},
+ {"timestamp": "2022-04-14T11:54:35+10:00", "duration": 581358},
+ {"timestamp": "2022-04-06T22:26:41+10:00", "duration": 65188},
+ {"timestamp": "2022-04-03T22:12:07+10:00", "duration": 654161},
+ {"timestamp": "2022-04-03T21:57:36+10:00", "duration": 798912},
+ {"timestamp": "2022-04-03T18:51:05+10:00", "duration": 67764},
+ {"timestamp": "2022-04-03T17:22:58+10:00", "duration": 641782},
+ {"timestamp": "2022-04-03T17:21:19+10:00", "duration": 69942},
+ {"timestamp": "2022-04-03T06:34:17+10:00", "duration": 232350},
+ {"timestamp": "2022-04-02T19:05:41+10:00", "duration": 47104},
+ {"timestamp": "2022-04-02T09:35:18+10:00", "duration": 258895},
+ {"timestamp": "2022-04-02T05:21:14+10:00", "duration": 63814},
+ {"timestamp": "2022-04-01T11:59:57+10:00", "duration": 586849},
+ {"timestamp": "2022-04-01T11:50:56+10:00", "duration": 457199},
+ {"timestamp": "2022-04-01T11:48:21+10:00", "duration": 51065},
+ {"timestamp": "2022-04-01T11:47:23+10:00", "duration": 41783},
+ {"timestamp": "2022-04-01T11:01:46+10:00", "duration": 73278},
+ {"timestamp": "2022-03-31T17:12:00+10:00", "duration": 45838},
+ {"timestamp": "2022-03-24T16:28:07+10:00", "duration": 122233},
+ {"timestamp": "2022-03-24T06:15:44+10:00", "duration": 5932791},
+ {"timestamp": "2022-03-23T17:01:37+10:00", "duration": 210322},
+ {"timestamp": "2022-03-23T16:11:27+10:00", "duration": 2608373},
+ {"timestamp": "2022-03-21T21:04:54+10:00", "duration": 296080},
+ ],
+ "events_count": 0,
+ "total_events": 0,
+ },
+ "user_settings": {
+ "storm_mode_enabled": True,
+ "powerwall_onboarding_settings_set": True,
+ "sync_grid_alert_enabled": False,
+ "breaker_alert_enabled": False,
+ },
+ "components": {
+ "solar": True,
+ "solar_type": "pv_panel",
+ "battery": True,
+ "grid": True,
+ "backup": True,
+ "gateway": "teg",
+ "load_meter": True,
+ "tou_capable": True,
+ "storm_mode_capable": True,
+ "flex_energy_request_capable": False,
+ "car_charging_data_supported": False,
+ "off_grid_vehicle_charging_reserve_supported": False,
+ "vehicle_charging_performance_view_enabled": False,
+ "vehicle_charging_solar_offset_view_enabled": False,
+ "battery_solar_offset_view_enabled": True,
+ "solar_value_enabled": True,
+ "energy_value_header": "Energy Value",
+ "energy_value_subheader": "Estimated Value",
+ "show_grid_import_battery_source_cards": True,
+ "backup_time_remaining_enabled": True,
+ "rate_plan_manager_supported": True,
+ "battery_type": "ac_powerwall",
+ "configurable": False,
+ "grid_services_enabled": False,
+ "customer_preferred_export_rule": "battery_ok",
+ "net_meter_mode": "battery_ok",
+ },
+ "default_real_mode": "self_consumption",
+ "operation": "self_consumption",
+ "installation_date": "2022-03-21T17:15:23+10:00",
+ "power_reading": [
+ {
+ "timestamp": "2022-08-18T15:10:20+10:00",
+ "load_power": 7010,
+ "solar_power": 6638,
+ "grid_power": 2,
+ "battery_power": 370,
+ "generator_power": 0,
+ }
+ ],
+ "battery_count": 0,
+}
+
+BATTERY_SUMMARY = {
+ "site_name": "Battery Home",
+ "id": "XXX",
+ "energy_left": 13610.736842105263,
+ "total_pack_energy": 14056,
+ "percentage_charged": 96.8322199922116,
+ "battery_power": 400,
+}
diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py
new file mode 100644
index 00000000..bb34ce04
--- /dev/null
+++ b/tests/test_binary_sensor.py
@@ -0,0 +1,130 @@
+"""Tests for the Tesla binary sensor."""
+
+from homeassistant.components.binary_sensor import (
+ DOMAIN as BINARY_SENSOR_DOMAIN,
+ BinarySensorDeviceClass,
+)
+from homeassistant.const import ATTR_DEVICE_CLASS
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("binary_sensor.my_model_s_parking_brake")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_parking_brake"
+
+ entry = entity_registry.async_get("binary_sensor.my_model_s_charger")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charger"
+
+ entry = entity_registry.async_get("binary_sensor.my_model_s_charging")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charging"
+
+ entry = entity_registry.async_get("binary_sensor.my_model_s_online")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_online"
+
+ entry = entity_registry.async_get("binary_sensor.battery_home_battery_charging")
+ assert entry.unique_id == "67890_battery_charging"
+
+ entry = entity_registry.async_get("binary_sensor.battery_home_grid_status")
+ assert entry.unique_id == "67890_grid_status"
+
+
+async def test_parking_brake(hass: HomeAssistant) -> None:
+ """Tests car parking brake is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.my_model_s_parking_brake")
+ assert state.state == "on"
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) is None
+
+
+async def test_charger_connection(hass: HomeAssistant) -> None:
+ """Tests car charger connection is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.my_model_s_charger")
+ assert state.state == "on"
+
+ # Not sure why this one is failing - checking device class works with other tests
+ # assert state.attributes.get(ATTR_DEVICE_CLASS) is BinarySensorDeviceClass.PLUG
+ assert (
+ state.attributes.get("charging_state")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charging_state"]
+ )
+ assert (
+ state.attributes.get("conn_charge_cable")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["conn_charge_cable"]
+ )
+ assert (
+ state.attributes.get("fast_charger_present")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["fast_charger_present"]
+ )
+ assert (
+ state.attributes.get("fast_charger_brand")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["fast_charger_brand"]
+ )
+ assert (
+ state.attributes.get("fast_charger_type")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["fast_charger_type"]
+ )
+
+
+async def test_charging(hass: HomeAssistant) -> None:
+ """Tests car charging is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.my_model_s_charging")
+ assert state.state == "on"
+
+ assert (
+ state.attributes.get(ATTR_DEVICE_CLASS)
+ == BinarySensorDeviceClass.BATTERY_CHARGING
+ )
+
+
+async def test_car_online(hass: HomeAssistant) -> None:
+ """Tests car online is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.my_model_s_online")
+ assert state.state == "on"
+
+ assert (
+ state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.CONNECTIVITY
+ )
+ assert state.attributes.get("vehicle_id") == str(
+ car_mock_data.VEHICLE["vehicle_id"]
+ )
+ assert state.attributes.get("vin") == car_mock_data.VEHICLE["vin"]
+ assert state.attributes.get("id") == str(car_mock_data.VEHICLE["id"])
+
+
+async def test_battery_charging(hass: HomeAssistant) -> None:
+ """Tests energy site battery charging is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.battery_home_battery_charging")
+ assert state.state == "off"
+
+ assert (
+ state.attributes.get(ATTR_DEVICE_CLASS)
+ == BinarySensorDeviceClass.BATTERY_CHARGING
+ )
+
+
+async def test_grid_status(hass: HomeAssistant) -> None:
+ """Tests energy site grid status is getting the correct value."""
+ await setup_platform(hass, BINARY_SENSOR_DOMAIN)
+
+ state = hass.states.get("binary_sensor.battery_home_grid_status")
+ assert state.state == "on"
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.POWER
diff --git a/tests/test_button.py b/tests/test_button.py
new file mode 100644
index 00000000..cf560fdb
--- /dev/null
+++ b/tests/test_button.py
@@ -0,0 +1,105 @@
+"""Tests for the Tesla button."""
+from unittest.mock import patch
+
+from homeassistant.components.button import (
+ DOMAIN as BUTTON_DOMAIN,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, BUTTON_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("button.my_model_s_horn")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_horn"
+
+ entry = entity_registry.async_get("button.my_model_s_flash_lights")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_flash_lights"
+
+ entry = entity_registry.async_get("button.my_model_s_wake_up")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_wake_up"
+
+ entry = entity_registry.async_get("button.my_model_s_force_data_update")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_force_data_update"
+
+ entry = entity_registry.async_get("button.my_model_s_homelink")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_homelink"
+
+
+async def test_horn_press(hass: HomeAssistant) -> None:
+ """Tests car horn button press."""
+ await setup_platform(hass, BUTTON_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.honk_horn") as mock_honk_horn:
+ assert await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: "button.my_model_s_horn"},
+ blocking=True,
+ )
+ mock_honk_horn.assert_awaited_once()
+
+
+async def test_flash_lights_press(hass: HomeAssistant) -> None:
+ """Tests car flash lights button press."""
+ await setup_platform(hass, BUTTON_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.flash_lights") as mock_flash_lights:
+ assert await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: "button.my_model_s_flash_lights"},
+ blocking=True,
+ )
+ mock_flash_lights.assert_awaited_once()
+
+
+async def test_wake_up_press(hass: HomeAssistant) -> None:
+ """Tests car wake up button press."""
+ await setup_platform(hass, BUTTON_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.wake_up") as mock_wake_up:
+ assert await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: "button.my_model_s_wake_up"},
+ blocking=True,
+ )
+ mock_wake_up.assert_awaited_once()
+
+
+async def test_force_data_update_press(hass: HomeAssistant) -> None:
+ """Tests car force data button press."""
+ await setup_platform(hass, BUTTON_DOMAIN)
+
+ with patch(
+ "custom_components.tesla_custom.base.TeslaCarEntity.update_controller"
+ ) as mock_force_data_update:
+ assert await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: "button.my_model_s_force_data_update"},
+ blocking=True,
+ )
+ mock_force_data_update.assert_awaited_once_with(wake_if_asleep=True, force=True)
+
+
+# async def test_trigger_homelink_press(hass: HomeAssistant) -> None:
+# """Tests car trigger homelink button press."""
+# await setup_platform(hass, BUTTON_DOMAIN)
+# # Need a way to enable this device before running tests (disabled by default)
+# with patch("teslajsonpy.car.TeslaCar.trigger_homelink") as mock_trigger_homelink:
+# assert await hass.services.async_call(
+# BUTTON_DOMAIN,
+# "press",
+# {ATTR_ENTITY_ID: "button.my_model_s_trigger_homelink"},
+# blocking=True,
+# )
+# mock_trigger_homelink.assert_awaited_once()
diff --git a/tests/test_climate.py b/tests/test_climate.py
new file mode 100644
index 00000000..28ef48d5
--- /dev/null
+++ b/tests/test_climate.py
@@ -0,0 +1,150 @@
+"""Tests for the Tesla climate."""
+from unittest.mock import patch
+
+from homeassistant.components.climate import (
+ DOMAIN as CLIMATE_DOMAIN,
+)
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+DEVICE_ID = "climate.my_model_s_hvac_climate_system"
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, CLIMATE_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get(DEVICE_ID)
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_hvac_climate_system"
+
+
+async def test_climate_properties(hass: HomeAssistant) -> None:
+ """Tests car climate properties."""
+ await setup_platform(hass, CLIMATE_DOMAIN)
+
+ state = hass.states.get(DEVICE_ID)
+
+ assert state.state == "off"
+
+ assert (
+ state.attributes.get("min_temp")
+ == car_mock_data.VEHICLE_DATA["climate_state"]["min_avail_temp"]
+ )
+ assert (
+ state.attributes.get("max_temp")
+ == car_mock_data.VEHICLE_DATA["climate_state"]["max_avail_temp"]
+ )
+ assert (
+ state.attributes.get("current_temperature")
+ == car_mock_data.VEHICLE_DATA["climate_state"]["inside_temp"]
+ )
+ assert (
+ state.attributes.get("temperature")
+ == car_mock_data.VEHICLE_DATA["climate_state"]["driver_temp_setting"]
+ )
+
+
+async def test_set_temperature(hass: HomeAssistant) -> None:
+ """Tests car setting temperature."""
+ await setup_platform(hass, CLIMATE_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.set_temperature") as mock_set_temperature:
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_temperature",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ ATTR_TEMPERATURE: 21.0,
+ },
+ blocking=True,
+ )
+ mock_set_temperature.assert_awaited_once_with(21.0)
+
+
+async def test_set_hvac_mode(hass: HomeAssistant) -> None:
+ """Tests car setting HVAC mode."""
+ await setup_platform(hass, CLIMATE_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.set_hvac_mode") as mock_set_hvac_mode:
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_hvac_mode",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ "hvac_mode": "heat_cool",
+ },
+ blocking=True,
+ )
+ mock_set_hvac_mode.assert_awaited_once_with("on")
+
+
+async def test_set_preset_mode(hass: HomeAssistant) -> None:
+ """Tests car setting HVAC mode."""
+ await setup_platform(hass, CLIMATE_DOMAIN)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.set_max_defrost"
+ ) as mock_set_max_defrost, patch(
+ "teslajsonpy.car.TeslaCar.defrost_mode", return_value=1
+ ):
+ # Test set preset_mode "Normal" with defrost_mode != 0
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_preset_mode",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ "preset_mode": "Normal",
+ },
+ blocking=True,
+ )
+ mock_set_max_defrost.assert_awaited_once_with(False)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.set_climate_keeper_mode"
+ ) as mock_set_climate_keeper_mode, patch(
+ "teslajsonpy.car.TeslaCar.climate_keeper_mode", return_value="on"
+ ):
+ # Test set preset_mode "Normal" with climate_keeper_mode != 0
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_preset_mode",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ "preset_mode": "Normal",
+ },
+ blocking=True,
+ )
+ mock_set_climate_keeper_mode.assert_awaited_once_with(0)
+
+ with patch("teslajsonpy.car.TeslaCar.set_max_defrost") as mock_set_max_defrost:
+ # Test set preset_mode "Defrost"
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_preset_mode",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ "preset_mode": "Defrost",
+ },
+ blocking=True,
+ )
+ mock_set_max_defrost.assert_awaited_once_with(2)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.set_climate_keeper_mode"
+ ) as mock_set_climate_keeper_mode:
+ # Test set preset_mode "Dog Mode"
+ assert await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ "set_preset_mode",
+ {
+ ATTR_ENTITY_ID: DEVICE_ID,
+ "preset_mode": "Dog Mode",
+ },
+ blocking=True,
+ )
+ mock_set_climate_keeper_mode.assert_awaited_once_with(2)
diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py
index f475572c..8d348734 100644
--- a/tests/test_config_flow.py
+++ b/tests/test_config_flow.py
@@ -18,6 +18,8 @@
from custom_components.tesla_custom.const import (
ATTR_POLLING_POLICY_CONNECTED,
CONF_EXPIRATION,
+ CONF_INCLUDE_ENERGYSITES,
+ CONF_INCLUDE_VEHICLES,
CONF_POLLING_POLICY,
CONF_WAKE_ON_START,
DEFAULT_POLLING_POLICY,
@@ -68,6 +70,9 @@ async def test_form(hass):
CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
CONF_DOMAIN: AUTH_DOMAIN,
+ CONF_INCLUDE_VEHICLES: True,
+ CONF_INCLUDE_ENERGYSITES: True,
+ "initial_setup": True,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -206,7 +211,12 @@ async def test_import(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={CONF_TOKEN: TEST_TOKEN, CONF_USERNAME: TEST_USERNAME},
+ data={
+ CONF_TOKEN: TEST_TOKEN,
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_INCLUDE_VEHICLES: True,
+ CONF_INCLUDE_ENERGYSITES: True,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TEST_USERNAME
diff --git a/tests/test_cover.py b/tests/test_cover.py
new file mode 100644
index 00000000..5b608c4f
--- /dev/null
+++ b/tests/test_cover.py
@@ -0,0 +1,80 @@
+"""Tests for the Tesla cover."""
+from unittest.mock import patch
+
+from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_CLOSE_COVER,
+ SERVICE_OPEN_COVER,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, COVER_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("cover.my_model_s_charger_door")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charger_door"
+
+ entry = entity_registry.async_get("cover.my_model_s_frunk")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_frunk"
+
+ entry = entity_registry.async_get("cover.my_model_s_trunk")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_trunk"
+
+
+async def test_charger_door(hass: HomeAssistant) -> None:
+ """Tests charger door cover."""
+ await setup_platform(hass, COVER_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.charge_port_door_open") as mock_open_cover:
+ assert await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.my_model_s_charger_door"},
+ blocking=True,
+ )
+ mock_open_cover.assert_awaited_once()
+
+ with patch("teslajsonpy.car.TeslaCar.charge_port_door_close") as mock_close_cover:
+ assert await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: "cover.my_model_s_charger_door"},
+ blocking=True,
+ )
+ mock_close_cover.assert_awaited_once()
+
+
+async def test_frunk(hass: HomeAssistant) -> None:
+ """Tests frunk cover."""
+ await setup_platform(hass, COVER_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.toggle_frunk") as mock_toggle_frunk:
+ assert await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.my_model_s_frunk"},
+ blocking=True,
+ )
+ mock_toggle_frunk.assert_awaited_once()
+
+
+async def test_trunk(hass: HomeAssistant) -> None:
+ """Tests trunk cover."""
+ await setup_platform(hass, COVER_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.toggle_trunk") as mock_toggle_trunk:
+ assert await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.my_model_s_trunk"},
+ blocking=True,
+ )
+ mock_toggle_trunk.assert_awaited_once()
diff --git a/tests/test_device_tracker.py b/tests/test_device_tracker.py
new file mode 100644
index 00000000..5f7b34ba
--- /dev/null
+++ b/tests/test_device_tracker.py
@@ -0,0 +1,35 @@
+"""Tests for the Tesla device tracker."""
+
+# from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
+# from homeassistant.core import HomeAssistant
+# from homeassistant.helpers import entity_registry as er
+
+# from .common import setup_platform
+
+# from .mock_data import car as car_mock_data
+
+
+# async def test_registry_entries(hass: HomeAssistant) -> None:
+# """Tests devices are registered in the entity registry."""
+# await setup_platform(hass, DEVICE_TRACKER_DOMAIN)
+
+# entity_registry = er.async_get(hass)
+
+# entry = entity_registry.async_get("device_tracker.my_model_s_location_tracker")
+# assert entry.unique_id == f"{car_mock_data.VIN.lower()}_location_tracker"
+
+
+# async def test_car_location(hass: HomeAssistant) -> None:
+# """Tests car location is getting the correct value."""
+# await setup_platform(hass, DEVICE_TRACKER_DOMAIN)
+
+# state = hass.states.get("device_tracker.my_model_s_location_tracker")
+
+# assert (
+# state.attributes.get("heading")
+# == car_mock_data.VEHICLE_DATA["drive_state"]["heading"]
+# )
+# assert (
+# state.attributes.get("speed")
+# == car_mock_data.VEHICLE_DATA["drive_state"]["speed"]
+# )
diff --git a/tests/test_lock.py b/tests/test_lock.py
new file mode 100644
index 00000000..a6563ecc
--- /dev/null
+++ b/tests/test_lock.py
@@ -0,0 +1,46 @@
+"""Tests for the Tesla lock."""
+from unittest.mock import patch
+
+from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_LOCK,
+ SERVICE_UNLOCK,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, LOCK_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("lock.my_model_s_doors")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_doors"
+
+
+async def test_car_door(hass: HomeAssistant) -> None:
+ """Tests car door lock."""
+ await setup_platform(hass, LOCK_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.unlock") as mock_unlock:
+ assert await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_UNLOCK,
+ {ATTR_ENTITY_ID: "lock.my_model_s_doors"},
+ blocking=True,
+ )
+ mock_unlock.assert_awaited_once()
+
+ with patch("teslajsonpy.car.TeslaCar.lock") as mock_unlock:
+ assert await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_LOCK,
+ {ATTR_ENTITY_ID: "lock.my_model_s_doors"},
+ blocking=True,
+ )
+ mock_unlock.assert_awaited_once()
diff --git a/tests/test_number.py b/tests/test_number.py
new file mode 100644
index 00000000..e8ec487f
--- /dev/null
+++ b/tests/test_number.py
@@ -0,0 +1,124 @@
+"""Tests for the Tesla number."""
+from unittest.mock import patch
+
+from teslajsonpy.const import BACKUP_RESERVE_MAX, BACKUP_RESERVE_MIN, CHARGE_CURRENT_MIN
+
+from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+from .mock_data import energysite as energysite_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("number.my_model_s_charge_limit")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charge_limit"
+
+ entry = entity_registry.async_get("number.my_model_s_charging_amps")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charging_amps"
+
+ entry = entity_registry.async_get("number.battery_home_backup_reserve")
+ assert entry.unique_id == "67890_backup_reserve"
+
+
+async def test_charge_limit(hass: HomeAssistant) -> None:
+ """Tests car charge limit is getting the correct value."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ state = hass.states.get("number.my_model_s_charge_limit")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["charge_state"]["charge_limit_soc"]
+ )
+
+ assert (
+ state.attributes.get("min")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charge_limit_soc_min"]
+ )
+
+ assert (
+ state.attributes.get("max")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charge_limit_soc_max"]
+ )
+
+
+async def test_set_charge_limit(hass: HomeAssistant) -> None:
+ """Tests car set charge limit."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.change_charge_limit"
+ ) as mock_change_charge_limit:
+ assert await hass.services.async_call(
+ NUMBER_DOMAIN,
+ "set_value",
+ {ATTR_ENTITY_ID: "number.my_model_s_charge_limit", "value": 50.0},
+ blocking=True,
+ )
+ mock_change_charge_limit.assert_awaited_once_with(50.0)
+
+
+async def test_charging_amps(hass: HomeAssistant) -> None:
+ """Tests car charging amps."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ state = hass.states.get("number.my_model_s_charging_amps")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["charge_state"]["charge_current_request"]
+ )
+
+ assert state.attributes.get("min") == CHARGE_CURRENT_MIN
+
+ assert (
+ state.attributes.get("max")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charge_current_request_max"]
+ )
+
+
+async def test_set_charging_amps(hass: HomeAssistant) -> None:
+ """Tests car set charging amps."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.set_charging_amps") as mock_set_charging_amps:
+ assert await hass.services.async_call(
+ NUMBER_DOMAIN,
+ "set_value",
+ {ATTR_ENTITY_ID: "number.my_model_s_charging_amps", "value": 15.0},
+ blocking=True,
+ )
+ mock_set_charging_amps.assert_awaited_once_with(15.0)
+
+
+async def test_backup_reserve(hass: HomeAssistant) -> None:
+ """Tests energy site backup reserve is getting the correct value."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ state = hass.states.get("number.battery_home_backup_reserve")
+ assert state.state == str(
+ energysite_mock_data.BATTERY_DATA["backup"]["backup_reserve_percent"]
+ )
+
+ assert state.attributes.get("min") == BACKUP_RESERVE_MIN
+ assert state.attributes.get("max") == BACKUP_RESERVE_MAX
+
+
+async def test_set_backup_reserve(hass: HomeAssistant) -> None:
+ """Tests energy site set backup reserve."""
+ await setup_platform(hass, NUMBER_DOMAIN)
+
+ with patch(
+ "teslajsonpy.energy.PowerwallSite.set_reserve_percent"
+ ) as mock_set_reserve_percent:
+ assert await hass.services.async_call(
+ NUMBER_DOMAIN,
+ "set_value",
+ {ATTR_ENTITY_ID: "number.battery_home_backup_reserve", "value": 20.0},
+ blocking=True,
+ )
+ mock_set_reserve_percent.assert_awaited_once_with(20.0)
diff --git a/tests/test_select.py b/tests/test_select.py
new file mode 100644
index 00000000..16fc3a25
--- /dev/null
+++ b/tests/test_select.py
@@ -0,0 +1,221 @@
+"""Tests for the Tesla select."""
+from unittest.mock import patch
+
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, SELECT_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("select.my_model_s_heated_seat_left")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_heated_seat_left"
+
+ entry = entity_registry.async_get("select.my_model_s_heated_seat_right")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_heated_seat_right"
+
+ entry = entity_registry.async_get("select.my_model_s_cabin_overheat_protection")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_cabin_overheat_protection"
+
+ entry = entity_registry.async_get("select.battery_home_grid_charging")
+ assert entry.unique_id == "67890_grid_charging"
+
+ entry = entity_registry.async_get("select.battery_home_energy_exports")
+ assert entry.unique_id == "67890_energy_exports"
+
+ entry = entity_registry.async_get("select.battery_home_operation_mode")
+ assert entry.unique_id == "67890_operation_mode"
+
+
+async def test_car_heated_seat_select(hass: HomeAssistant) -> None:
+ """Tests car heated seat select."""
+ await setup_platform(hass, SELECT_DOMAIN)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.remote_seat_heater_request"
+ ) as mock_remote_seat_heater_request:
+ # Test selecting "Off"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.my_model_s_heated_seat_left", "option": "Off"},
+ blocking=True,
+ )
+ mock_remote_seat_heater_request.assert_awaited_once_with(0, 0)
+ # Test selecting "Low"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.my_model_s_heated_seat_left", "option": "Low"},
+ blocking=True,
+ )
+ mock_remote_seat_heater_request.assert_awaited_with(1, 0)
+ # Test selecting "Medium"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.my_model_s_heated_seat_left", "option": "Medium"},
+ blocking=True,
+ )
+ mock_remote_seat_heater_request.assert_awaited_with(2, 0)
+ # Test selecting "High"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: "select.my_model_s_heated_seat_left", "option": "High"},
+ blocking=True,
+ )
+ mock_remote_seat_heater_request.assert_awaited_with(3, 0)
+
+
+async def test_cabin_overheat_protection(hass: HomeAssistant) -> None:
+ """Tests car cabin overheat protection select."""
+ await setup_platform(hass, SELECT_DOMAIN)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.set_cabin_overheat_protection"
+ ) as mock_set_cabin_overheat_protection:
+ # Test selecting "On"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.my_model_s_cabin_overheat_protection",
+ "option": "On",
+ },
+ blocking=True,
+ )
+ mock_set_cabin_overheat_protection.assert_awaited_once_with("On")
+ # Test selecting "Off"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.my_model_s_cabin_overheat_protection",
+ "option": "Off",
+ },
+ blocking=True,
+ )
+ mock_set_cabin_overheat_protection.assert_awaited_with("Off")
+ # Test selecting "No A/C"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.my_model_s_cabin_overheat_protection",
+ "option": "No A/C",
+ },
+ blocking=True,
+ )
+ mock_set_cabin_overheat_protection.assert_awaited_with("No A/C")
+
+
+async def test_grid_charging(hass: HomeAssistant) -> None:
+ """Tests energy site grid charging select."""
+ await setup_platform(hass, SELECT_DOMAIN)
+
+ with patch(
+ "teslajsonpy.energy.SolarPowerwallSite.set_grid_charging"
+ ) as mock_set_grid_charging:
+ # Test selecting "Yes"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_grid_charging",
+ "option": "Yes",
+ },
+ blocking=True,
+ )
+ mock_set_grid_charging.assert_awaited_once_with(True)
+ # Test selecting "No"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_grid_charging",
+ "option": "No",
+ },
+ blocking=True,
+ )
+ mock_set_grid_charging.assert_awaited_with(False)
+
+
+async def test_energy_exports(hass: HomeAssistant) -> None:
+ """Tests energy site energy exports select."""
+ await setup_platform(hass, SELECT_DOMAIN)
+
+ with patch(
+ "teslajsonpy.energy.SolarPowerwallSite.set_export_rule"
+ ) as mock_set_export_rule:
+ # Test selecting "Solar"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_energy_exports",
+ "option": "Solar",
+ },
+ blocking=True,
+ )
+ mock_set_export_rule.assert_awaited_once_with("pv_only")
+ # Test selecting "Everything"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_energy_exports",
+ "option": "Everything",
+ },
+ blocking=True,
+ )
+ mock_set_export_rule.assert_awaited_with("battery_ok")
+
+
+async def test_operation_mode(hass: HomeAssistant) -> None:
+ """Tests energy site operation mode select."""
+ await setup_platform(hass, SELECT_DOMAIN)
+
+ with patch(
+ "teslajsonpy.energy.SolarPowerwallSite.set_operation_mode"
+ ) as mock_set_operation_mode:
+ # Test selecting "Self-Powered"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_operation_mode",
+ "option": "Self-Powered",
+ },
+ blocking=True,
+ )
+ mock_set_operation_mode.assert_awaited_once_with("self_consumption")
+ # Test selecting "Time-Based Control"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_operation_mode",
+ "option": "Time-Based Control",
+ },
+ blocking=True,
+ )
+ mock_set_operation_mode.assert_awaited_with("autonomous")
+ # Test selecting "Backup"
+ assert await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {
+ ATTR_ENTITY_ID: "select.battery_home_operation_mode",
+ "option": "Backup",
+ },
+ blocking=True,
+ )
+ mock_set_operation_mode.assert_awaited_with("backup")
diff --git a/tests/test_sensor.py b/tests/test_sensor.py
new file mode 100644
index 00000000..3fcd7c06
--- /dev/null
+++ b/tests/test_sensor.py
@@ -0,0 +1,290 @@
+"""Tests for the Tesla sensor device."""
+
+from homeassistant.components.sensor import (
+ DOMAIN as SENSOR_DOMAIN,
+ SensorDeviceClass,
+ SensorStateClass,
+)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_UNIT_OF_MEASUREMENT,
+ ENERGY_KILO_WATT_HOUR,
+ ENERGY_WATT_HOUR,
+ LENGTH_KILOMETERS,
+ PERCENTAGE,
+ POWER_WATT,
+ TEMP_CELSIUS,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+from .mock_data import energysite as energysite_mock_data
+
+ATTR_STATE_CLASS = "state_class"
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("sensor.my_model_s_battery")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_battery"
+
+ entry = entity_registry.async_get("sensor.my_model_s_charging_rate")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charging_rate"
+
+ entry = entity_registry.async_get("sensor.my_model_s_energy_added")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_energy_added"
+
+ entry = entity_registry.async_get("sensor.my_model_s_odometer")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_odometer"
+
+ entry = entity_registry.async_get("sensor.my_model_s_temperature_outside")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_temperature_outside"
+
+ entry = entity_registry.async_get("sensor.my_model_s_temperature_inside")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_temperature_inside"
+
+ entry = entity_registry.async_get("sensor.my_home_solar_power")
+ assert entry.unique_id == "12345_solar_power"
+
+ entry = entity_registry.async_get("sensor.my_home_grid_power")
+ assert entry.unique_id == "12345_grid_power"
+
+ entry = entity_registry.async_get("sensor.my_home_load_power")
+ assert entry.unique_id == "12345_load_power"
+
+ entry = entity_registry.async_get("sensor.battery_home_battery_power")
+ assert entry.unique_id == "67890_battery_power"
+
+ entry = entity_registry.async_get("sensor.battery_home_battery")
+ assert entry.unique_id == "67890_battery"
+
+ entry = entity_registry.async_get("sensor.battery_home_battery_remaining")
+ assert entry.unique_id == "67890_battery_remaining"
+
+ entry = entity_registry.async_get("sensor.battery_home_backup_reserve")
+ assert entry.unique_id == "67890_backup_reserve"
+
+
+async def test_battery(hass: HomeAssistant) -> None:
+ """Tests battery is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.battery_home_battery")
+ assert state.state == str(
+ round(energysite_mock_data.BATTERY_SUMMARY["percentage_charged"])
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+
+
+async def test_battery_power_value(hass: HomeAssistant) -> None:
+ """Tests battery_power is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.battery_home_battery_power")
+ assert state.state == str(
+ round(energysite_mock_data.BATTERY_DATA["power_reading"][0]["battery_power"])
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+
+
+async def test_battery_remaining(hass: HomeAssistant) -> None:
+ """Tests battery remaining is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.battery_home_battery_remaining")
+ assert state.state == str(
+ round(energysite_mock_data.BATTERY_SUMMARY["energy_left"])
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR
+
+
+async def test_backup_reserve(hass: HomeAssistant) -> None:
+ """Tests backup reserve is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.battery_home_backup_reserve")
+ assert state.state == str(
+ round(energysite_mock_data.BATTERY_DATA["backup"]["backup_reserve_percent"])
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+
+
+async def test_battery_value(hass: HomeAssistant) -> None:
+ """Tests battery is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_battery")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["charge_state"]["battery_level"]
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+
+
+async def test_charger_energy_value(hass: HomeAssistant) -> None:
+ """Tests charger_energy is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_energy_added")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["charge_state"]["charge_energy_added"]
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+
+
+async def test_charger_power_value(hass: HomeAssistant) -> None:
+ """Tests charger_power is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_charger_power")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["charge_state"]["charger_power"]
+ )
+
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+
+ assert (
+ state.attributes.get("charger_amps_request")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charge_current_request"]
+ )
+ assert (
+ state.attributes.get("charger_amps_actual")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charger_actual_current"]
+ )
+ assert (
+ state.attributes.get("charger_volts")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charger_voltage"]
+ )
+ assert (
+ state.attributes.get("charger_phases")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["charger_phases"]
+ )
+
+
+async def test_charger_rate_value(hass: HomeAssistant) -> None:
+ """Tests charger_rate is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_charging_rate")
+ # Test state against km/hr
+ # Tesla API returns in miles so manually set charge rate to km/hr
+ assert state.state == "37.34"
+
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+
+ assert (
+ state.attributes.get("time_left")
+ == car_mock_data.VEHICLE_DATA["charge_state"]["time_to_full_charge"]
+ )
+
+
+async def test_grid_power_value(hass: HomeAssistant) -> None:
+ """Tests grid_power is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_home_grid_power")
+ assert state.state == str(round(energysite_mock_data.SITE_DATA["grid_power"]))
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+
+
+async def test_inside_temp_value(hass: HomeAssistant) -> None:
+ """Tests inside_temp is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_temperature_inside")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["climate_state"]["inside_temp"]
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
+
+
+async def test_load_power_value(hass: HomeAssistant) -> None:
+ """Tests load_power is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_home_load_power")
+ assert state.state == str(round(energysite_mock_data.SITE_DATA["load_power"]))
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+
+
+async def test_odometer_value(hass: HomeAssistant) -> None:
+ """Tests odometer is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_odometer")
+ # Test state against odometer in kilometers
+ # Tesla API returns in miles so manually set range in kilometers
+ assert state.state == "114127.59"
+
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
+
+
+async def test_outside_temp_value(hass: HomeAssistant) -> None:
+ """Tests outside_temp is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_temperature_outside")
+ assert state.state == str(
+ car_mock_data.VEHICLE_DATA["climate_state"]["outside_temp"]
+ )
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
+
+
+async def test_range_value(hass: HomeAssistant) -> None:
+ """Tests range is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_model_s_range")
+ # Test state against range in kilometers
+ # Tesla API returns in miles so manually set range in kilometers
+ assert state.state == "272.11"
+
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
+
+
+async def test_solar_power_value(hass: HomeAssistant) -> None:
+ """Tests solar_power is getting the correct value."""
+ await setup_platform(hass, SENSOR_DOMAIN)
+
+ state = hass.states.get("sensor.my_home_solar_power")
+ assert state.state == str(round(energysite_mock_data.SITE_DATA["solar_power"]))
+
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
+ assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
diff --git a/tests/test_switch.py b/tests/test_switch.py
new file mode 100644
index 00000000..80854a26
--- /dev/null
+++ b/tests/test_switch.py
@@ -0,0 +1,117 @@
+"""Tests for the Tesla switch."""
+from unittest.mock import patch
+
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, SWITCH_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("switch.my_model_s_heated_steering")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_heated_steering"
+
+ entry = entity_registry.async_get("switch.my_model_s_polling")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_polling"
+
+ entry = entity_registry.async_get("switch.my_model_s_charger")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_charger"
+
+ entry = entity_registry.async_get("switch.my_model_s_sentry_mode")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_sentry_mode"
+
+
+async def test_heated_steering(hass: HomeAssistant) -> None:
+ """Tests car heated steering switch."""
+ await setup_platform(hass, SWITCH_DOMAIN)
+
+ with patch(
+ "teslajsonpy.car.TeslaCar.set_heated_steering_wheel"
+ ) as mock_seat_heated_steering_wheel:
+ # Test switch on
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.my_model_s_heated_steering"},
+ blocking=True,
+ )
+ mock_seat_heated_steering_wheel.assert_awaited_once_with(True)
+ # Test switch off
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.my_model_s_heated_steering"},
+ blocking=True,
+ )
+ mock_seat_heated_steering_wheel.assert_awaited_with(False)
+
+
+# async def test_polling(hass: HomeAssistant) -> None:
+# """Tests polling switch."""
+# await setup_platform(hass, SWITCH_DOMAIN)
+
+# with patch(
+# "teslajsonpy.Controller.set_updates"
+# ) as mock_controller_set_updates, patch("teslajsonpy.Controller.get_updates"):
+
+# assert await hass.services.async_call(
+# SWITCH_DOMAIN,
+# SERVICE_TURN_ON,
+# {ATTR_ENTITY_ID: "switch.my_model_s_polling"},
+# blocking=True,
+# )
+# mock_controller_set_updates.assert_awaited_once_with(car_mock_data.VIN, True)
+
+
+async def test_charger(hass: HomeAssistant) -> None:
+ """Tests car charger switch."""
+ await setup_platform(hass, SWITCH_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.start_charge") as mock_start_charge:
+ # Test switch on
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.my_model_s_charger"},
+ blocking=True,
+ )
+ mock_start_charge.assert_awaited_once()
+
+ with patch("teslajsonpy.car.TeslaCar.stop_charge") as mock_start_charge:
+ # Test switch off
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.my_model_s_charger"},
+ blocking=True,
+ )
+ mock_start_charge.assert_awaited()
+
+
+async def test_sentry_mode(hass: HomeAssistant) -> None:
+ """Tests car sentry mode switch."""
+ await setup_platform(hass, SWITCH_DOMAIN)
+
+ with patch("teslajsonpy.car.TeslaCar.set_sentry_mode") as mock_set_sentry_mode:
+ # Test switch on
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.my_model_s_sentry_mode"},
+ blocking=True,
+ )
+ mock_set_sentry_mode.assert_awaited_once_with(True)
+ # Test switch off
+ assert await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.my_model_s_sentry_mode"},
+ blocking=True,
+ )
+ mock_set_sentry_mode.assert_awaited_with(False)
diff --git a/tests/test_tesla_device.py b/tests/test_tesla_device.py
deleted file mode 100644
index e696b3ca..00000000
--- a/tests/test_tesla_device.py
+++ /dev/null
@@ -1,135 +0,0 @@
-"""Test the Tesla base class."""
-from unittest.mock import MagicMock, Mock
-
-import pytest
-from teslajsonpy.exceptions import IncompleteCredentials
-
-from custom_components.tesla_custom.const import DOMAIN
-from custom_components.tesla_custom.tesla_device import TeslaDevice
-
-
-@pytest.fixture
-def tesla_api_mock():
- """Create tesla_device mock for the API."""
- tesla_api_mock = Mock(uniq_name="uniq_id")
- tesla_api_mock.name = "name"
- tesla_api_mock.unique_id = "uniq_id"
- tesla_api_mock.id.return_value = 1
- tesla_api_mock.car_name.return_value = "car_name"
- tesla_api_mock.car_type = "car_type"
- tesla_api_mock.car_version = "car_version"
- tesla_api_mock.type = "battery sensor"
- tesla_api_mock.device_type = None
- tesla_api_mock.has_battery.return_value = True
- tesla_api_mock.battery_level.return_value = 100
- tesla_api_mock.battery_charging.return_value = True
- tesla_api_mock.attrs = {}
- tesla_api_mock.refresh.return_value = True
- tesla_api_mock.enabled_by_default = True
- return tesla_api_mock
-
-
-@pytest.fixture
-def tesla_device_mock(tesla_api_mock):
- """Mock tesla_device instance."""
- coordinator = Mock(last_update_success=True)
- return TeslaDevice(tesla_api_mock, coordinator)
-
-
-@pytest.fixture
-def tesla_inherited_mock(tesla_api_mock):
- """Mock tesla_device instance to test decorator."""
-
- class testClass(TeslaDevice):
- """Test class with two functions."""
-
- @TeslaDevice.Decorators.check_for_reauth
- async def need_reauth(self):
- """Raise incomplete credentials."""
- raise IncompleteCredentials("TEST")
-
- @TeslaDevice.Decorators.check_for_reauth
- async def no_reauth(self):
- """Return True."""
- return True
-
- coordinator = Mock(last_update_success=True)
- return testClass(tesla_api_mock, coordinator)
-
-
-def test_tesla_init(tesla_device_mock):
- """Test init."""
- assert tesla_device_mock.tesla_device is not None
- assert tesla_device_mock.config_entry_id is None
- assert tesla_device_mock.name == "name"
- assert tesla_device_mock.unique_id == "uniq_id"
- assert tesla_device_mock.icon == "mdi:battery"
- assert tesla_device_mock.device_info == {
- "identifiers": {(DOMAIN, 1)},
- "manufacturer": "Tesla",
- "model": "car_type",
- "name": "car_name",
- "sw_version": "car_version",
- }
- assert tesla_device_mock.extra_state_attributes == {
- "battery_charging": True,
- "battery_level": 100,
- }
- assert tesla_device_mock.entity_registry_enabled_default is True
-
-
-async def test_tesla_added_to_hass(tesla_device_mock):
- """Test added to hass."""
- tesla_device_mock.hass = MagicMock()
- await tesla_device_mock.async_added_to_hass()
- assert tesla_device_mock.config_entry_id is not None
-
-
-async def test_tesla_refresh(tesla_device_mock):
- """Test refresh."""
- tesla_device_mock.hass = MagicMock()
- tesla_device_mock.entity_id = "asdf"
- tesla_device_mock.tesla_device.attrs = {"refreshed": True}
-
- tesla_device_mock.refresh()
- assert tesla_device_mock.tesla_device.refresh.is_called_once()
- assert tesla_device_mock.extra_state_attributes == {
- "refreshed": True,
- "battery_charging": True,
- "battery_level": 100,
- }
-
-
-def test_tesla_inherited_init(tesla_inherited_mock):
- """Test inherited device init."""
- assert tesla_inherited_mock.tesla_device is not None
- assert tesla_inherited_mock.config_entry_id is None
- assert tesla_inherited_mock.name == "name"
- assert tesla_inherited_mock.unique_id == "uniq_id"
- assert tesla_inherited_mock.icon == "mdi:battery"
- assert tesla_inherited_mock.device_info == {
- "identifiers": {(DOMAIN, 1)},
- "manufacturer": "Tesla",
- "model": "car_type",
- "name": "car_name",
- "sw_version": "car_version",
- }
- assert tesla_inherited_mock.extra_state_attributes == {
- "battery_charging": True,
- "battery_level": 100,
- }
- assert tesla_inherited_mock.entity_registry_enabled_default is True
-
-
-async def test_tesla_inherited_reauth_raised(tesla_inherited_mock):
- """Test need for reauth results in reload."""
- tesla_device_mock.hass = MagicMock()
- assert await tesla_inherited_mock.need_reauth() is None
- assert tesla_device_mock.hass.config_entries.async_reload.called_once()
-
-
-async def test_tesla_inherited_no_reauth_raised(tesla_inherited_mock):
- """Test no need for reauth returns value."""
- tesla_device_mock.hass = MagicMock()
- assert await tesla_inherited_mock.no_reauth() is True
- assert not tesla_device_mock.hass.config_entries.async_reload.called
diff --git a/tests/test_update.py b/tests/test_update.py
new file mode 100644
index 00000000..88bc57fd
--- /dev/null
+++ b/tests/test_update.py
@@ -0,0 +1,54 @@
+"""Tests for the Tesla update."""
+from unittest.mock import patch
+
+from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
+
+# from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .common import setup_platform
+from .mock_data import car as car_mock_data
+
+
+async def test_registry_entries(hass: HomeAssistant) -> None:
+ """Tests devices are registered in the entity registry."""
+ await setup_platform(hass, UPDATE_DOMAIN)
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get("update.my_model_s_software_update")
+ assert entry.unique_id == f"{car_mock_data.VIN.lower()}_software_update"
+
+
+async def test_software_update_properties(hass: HomeAssistant) -> None:
+ """Tests software update properties."""
+ await setup_platform(hass, UPDATE_DOMAIN)
+
+ version = car_mock_data.VEHICLE_DATA["vehicle_state"]["software_update"]["version"]
+
+ state = hass.states.get("update.my_model_s_software_update")
+ # assert state.state == str(car_mock_data.VEHICLE_STATE["software_update"])
+
+ assert state.attributes.get("latest_version") == version
+
+ assert (
+ state.attributes.get("release_url")
+ == f"https://www.notateslaapp.com/software-updates/version/{version}/release-notes"
+ )
+
+
+# async def test_install(hass: HomeAssistant) -> None:
+# """Tests install update."""
+# await setup_platform(hass, UPDATE_DOMAIN)
+
+# with patch(
+# "teslajsonpy.car.TeslaCar.schedule_software_update"
+# ) as mock_schedule_software_update:
+
+# assert await hass.services.async_call(
+# UPDATE_DOMAIN,
+# "install",
+# {ATTR_ENTITY_ID: "update.my_model_s_software_update"},
+# blocking=True,
+# )
+# mock_schedule_software_update.assert_awaited_once()