From 6924192523fcb02f3f22f17d70eb11fceb0ace85 Mon Sep 17 00:00:00 2001 From: jrester <31157644+jrester@users.noreply.github.com> Date: Mon, 13 Apr 2020 21:59:50 +0200 Subject: [PATCH] Use updated powerwall client API library (#34139) * Use updated powerwall client API library * Increase instant_power precision to 3 * Add @jrester as code owner for powerwall --- CODEOWNERS | 2 +- .../components/powerwall/__init__.py | 28 ++++----- .../components/powerwall/binary_sensor.py | 24 +++----- .../components/powerwall/config_flow.py | 16 ++---- homeassistant/components/powerwall/entity.py | 30 +++------- .../components/powerwall/manifest.json | 4 +- homeassistant/components/powerwall/sensor.py | 21 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/powerwall/mocks.py | 57 +++++++++++++------ .../powerwall/test_binary_sensor.py | 4 +- .../components/powerwall/test_config_flow.py | 23 +++----- tests/components/powerwall/test_sensor.py | 8 +-- 13 files changed, 104 insertions(+), 117 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e13573aa774..dd33491e2b4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -292,7 +292,7 @@ homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/plum_lightpad/* @ColinHarrington homeassistant/components/point/* @fredrike -homeassistant/components/powerwall/* @bdraco +homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index d5c7a534180..57ae6f0e260 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -4,12 +4,7 @@ from datetime import timedelta import logging import requests -from tesla_powerwall import ( - ApiError, - MetersResponse, - PowerWall, - PowerWallUnreachableError, -) +from tesla_powerwall import ApiError, Powerwall, PowerwallUnreachableError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -67,10 +62,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() - power_wall = PowerWall(entry.data[CONF_IP_ADDRESS], http_session=http_session) + power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session) try: powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall) - except (PowerWallUnreachableError, ApiError, ConnectionError): + except (PowerwallUnreachableError, ApiError, ConnectionError): http_session.close() raise ConfigEntryNotReady @@ -108,22 +103,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): def call_base_info(power_wall): """Wrap powerwall properties to be a callable.""" return { - POWERWALL_API_SITE_INFO: power_wall.site_info, - POWERWALL_API_STATUS: power_wall.status, - POWERWALL_API_DEVICE_TYPE: power_wall.device_type, + POWERWALL_API_SITE_INFO: power_wall.get_site_info(), + POWERWALL_API_STATUS: power_wall.get_status(), + POWERWALL_API_DEVICE_TYPE: power_wall.get_device_type(), } def _fetch_powerwall_data(power_wall): """Process and update powerwall data.""" - meters = power_wall.meters return { - POWERWALL_API_CHARGE: power_wall.charge, - POWERWALL_API_SITEMASTER: power_wall.sitemaster, - POWERWALL_API_METERS: { - meter: MetersResponse(meters[meter]) for meter in meters - }, - POWERWALL_API_GRID_STATUS: power_wall.grid_status, + POWERWALL_API_CHARGE: power_wall.get_charge(), + POWERWALL_API_SITEMASTER: power_wall.get_sitemaster(), + POWERWALL_API_METERS: power_wall.get_meters(), + POWERWALL_API_GRID_STATUS: power_wall.get_grid_status(), } diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 3b73caecacd..f088a257574 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -1,6 +1,8 @@ """Support for August sensors.""" import logging +from tesla_powerwall import GridStatus + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorDevice, @@ -17,13 +19,7 @@ from .const import ( POWERWALL_API_SITE_INFO, POWERWALL_API_SITEMASTER, POWERWALL_API_STATUS, - POWERWALL_CONNECTED_KEY, POWERWALL_COORDINATOR, - POWERWALL_GRID_ONLINE, - POWERWALL_RUNNING_KEY, - SITE_INFO_GRID_CODE, - SITE_INFO_NOMINAL_SYSTEM_POWER_KW, - SITE_INFO_REGION, ) from .entity import PowerWallEntity @@ -71,17 +67,15 @@ class PowerWallRunningSensor(PowerWallEntity, BinarySensorDevice): @property def is_on(self): """Get the powerwall running state.""" - return self._coordinator.data[POWERWALL_API_SITEMASTER][POWERWALL_RUNNING_KEY] + return self._coordinator.data[POWERWALL_API_SITEMASTER].running @property def device_state_attributes(self): """Return the device specific state attributes.""" return { - ATTR_REGION: self._site_info[SITE_INFO_REGION], - ATTR_GRID_CODE: self._site_info[SITE_INFO_GRID_CODE], - ATTR_NOMINAL_SYSTEM_POWER: self._site_info[ - SITE_INFO_NOMINAL_SYSTEM_POWER_KW - ], + ATTR_REGION: self._site_info.region, + ATTR_GRID_CODE: self._site_info.grid_code, + ATTR_NOMINAL_SYSTEM_POWER: self._site_info.nominal_system_power_kW, } @@ -106,7 +100,7 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorDevice): @property def is_on(self): """Get the powerwall connected to tesla state.""" - return self._coordinator.data[POWERWALL_API_SITEMASTER][POWERWALL_CONNECTED_KEY] + return self._coordinator.data[POWERWALL_API_SITEMASTER].connected_to_tesla class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): @@ -130,6 +124,4 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): @property def is_on(self): """Grid is online.""" - return ( - self._coordinator.data[POWERWALL_API_GRID_STATUS] == POWERWALL_GRID_ONLINE - ) + return self._coordinator.data[POWERWALL_API_GRID_STATUS] == GridStatus.CONNECTED diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 7e1b3eb3fb1..403075989e9 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,14 +1,13 @@ """Config flow for Tesla Powerwall integration.""" import logging -from tesla_powerwall import ApiError, PowerWall, PowerWallUnreachableError +from tesla_powerwall import ApiError, Powerwall, PowerwallUnreachableError import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_IP_ADDRESS from .const import DOMAIN # pylint:disable=unused-import -from .const import POWERWALL_SITE_NAME _LOGGER = logging.getLogger(__name__) @@ -21,20 +20,15 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - power_wall = PowerWall(data[CONF_IP_ADDRESS]) + power_wall = Powerwall(data[CONF_IP_ADDRESS]) try: - site_info = await hass.async_add_executor_job(call_site_info, power_wall) - except (PowerWallUnreachableError, ApiError, ConnectionError): + site_info = await hass.async_add_executor_job(power_wall.get_site_info) + except (PowerwallUnreachableError, ApiError, ConnectionError): raise CannotConnect # Return info that you want to store in the config entry. - return {"title": site_info[POWERWALL_SITE_NAME]} - - -def call_site_info(power_wall): - """Wrap site_info to be a callable.""" - return power_wall.site_info + return {"title": site_info.site_name} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index c09a1aca612..9e09ed06c4b 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -2,17 +2,7 @@ from homeassistant.helpers.entity import Entity -from .const import ( - DEVICE_TYPE_DEVICE_TYPE, - DOMAIN, - MANUFACTURER, - MODEL, - POWERWALL_SITE_NAME, - SITE_INFO_GRID_CODE, - SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH, - SITE_INFO_UTILITY, - STATUS_VERSION, -) +from .const import DOMAIN, MANUFACTURER, MODEL class PowerWallEntity(Entity): @@ -23,13 +13,13 @@ class PowerWallEntity(Entity): super().__init__() self._coordinator = coordinator self._site_info = site_info - self._device_type = device_type.get(DEVICE_TYPE_DEVICE_TYPE) - self._version = status.get(STATUS_VERSION) + self._device_type = device_type + self._version = status.version # This group of properties will be unique to to the site unique_group = ( - site_info[SITE_INFO_UTILITY], - site_info[SITE_INFO_GRID_CODE], - str(site_info[SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH]), + site_info.utility, + site_info.grid_code, + str(site_info.nominal_system_energy_kWh), ) self.base_unique_id = "_".join(unique_group) @@ -38,15 +28,13 @@ class PowerWallEntity(Entity): """Powerwall device info.""" device_info = { "identifiers": {(DOMAIN, self.base_unique_id)}, - "name": self._site_info[POWERWALL_SITE_NAME], + "name": self._site_info.site_name, "manufacturer": MANUFACTURER, } model = MODEL - if self._device_type: - model += f" ({self._device_type})" + model += f" ({self._device_type.name})" device_info["model"] = model - if self._version: - device_info["sw_version"] = self._version + device_info["sw_version"] = self._version return device_info @property diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 9a302fbcad1..de9da698c7c 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.1.3"], - "codeowners": ["@bdraco"] + "requirements": ["tesla-powerwall==0.2.3"], + "codeowners": ["@bdraco", "@jrester"] } diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 72dbd38a418..3b3d3b9cd32 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -1,6 +1,8 @@ """Support for August sensors.""" import logging +from tesla_powerwall import MeterType + from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, @@ -37,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): status = powerwall_data[POWERWALL_API_STATUS] entities = [] - for meter in coordinator.data[POWERWALL_API_METERS]: + for meter in MeterType: entities.append( PowerWallEnergySensor(meter, coordinator, site_info, status, device_type) ) @@ -73,13 +75,13 @@ class PowerWallChargeSensor(PowerWallEntity): @property def state(self): """Get the current value in percentage.""" - return round(self._coordinator.data[POWERWALL_API_CHARGE], 3) + return self._coordinator.data[POWERWALL_API_CHARGE] class PowerWallEnergySensor(PowerWallEntity): """Representation of an Powerwall Energy sensor.""" - def __init__(self, meter, coordinator, site_info, status, device_type): + def __init__(self, meter: MeterType, coordinator, site_info, status, device_type): """Initialize the sensor.""" super().__init__(coordinator, site_info, status, device_type) self._meter = meter @@ -92,7 +94,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def name(self): """Device Name.""" - return f"Powerwall {self._meter.title()} Now" + return f"Powerwall {self._meter.value.title()} Now" @property def device_class(self): @@ -102,18 +104,21 @@ class PowerWallEnergySensor(PowerWallEntity): @property def unique_id(self): """Device Uniqueid.""" - return f"{self.base_unique_id}_{self._meter}_instant_power" + return f"{self.base_unique_id}_{self._meter.value}_instant_power" @property def state(self): """Get the current value in kW.""" - meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] - return round(float(meter.instant_power / 1000), 3) + return ( + self._coordinator.data[POWERWALL_API_METERS] + .get(self._meter) + .get_power(precision=3) + ) @property def device_state_attributes(self): """Return the device specific state attributes.""" - meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] + meter = self._coordinator.data[POWERWALL_API_METERS].get(self._meter) return { ATTR_FREQUENCY: meter.frequency, ATTR_ENERGY_EXPORTED: meter.energy_exported, diff --git a/requirements_all.txt b/requirements_all.txt index 25d76e07c66..c05a5c178ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.powerwall -tesla-powerwall==0.1.3 +tesla-powerwall==0.2.3 # homeassistant.components.tesla teslajsonpy==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9c69c3efb..bf7ffbc4d97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.powerwall -tesla-powerwall==0.1.3 +tesla-powerwall==0.2.3 # homeassistant.components.tesla teslajsonpy==0.6.0 diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index aba6ecfeb23..afe9e130379 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -3,7 +3,16 @@ import json import os -from asynctest import MagicMock, PropertyMock +from asynctest import MagicMock, Mock +from tesla_powerwall import ( + DeviceType, + GridStatus, + MetersAggregateResponse, + Powerwall, + PowerwallStatusResponse, + SiteInfoResponse, + SitemasterResponse, +) from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS @@ -20,13 +29,13 @@ async def _mock_powerwall_with_fixtures(hass): device_type = await _async_load_json_fixture(hass, "device_type.json") return _mock_powerwall_return_value( - site_info=site_info, - charge=47.31993232, - sitemaster=sitemaster, - meters=meters, - grid_status="SystemGridConnected", - status=status, - device_type=device_type, + site_info=SiteInfoResponse(site_info), + charge=47, + sitemaster=SitemasterResponse(sitemaster), + meters=MetersAggregateResponse(meters), + grid_status=GridStatus.CONNECTED, + status=PowerwallStatusResponse(status), + device_type=DeviceType(device_type["device_type"]), ) @@ -39,21 +48,33 @@ def _mock_powerwall_return_value( status=None, device_type=None, ): - powerwall_mock = MagicMock() - type(powerwall_mock).site_info = PropertyMock(return_value=site_info) - type(powerwall_mock).charge = PropertyMock(return_value=charge) - type(powerwall_mock).sitemaster = PropertyMock(return_value=sitemaster) - type(powerwall_mock).meters = PropertyMock(return_value=meters) - type(powerwall_mock).grid_status = PropertyMock(return_value=grid_status) - type(powerwall_mock).status = PropertyMock(return_value=status) - type(powerwall_mock).device_type = PropertyMock(return_value=device_type) + powerwall_mock = MagicMock(Powerwall("1.2.3.4")) + powerwall_mock.get_site_info = Mock(return_value=site_info) + powerwall_mock.get_charge = Mock(return_value=charge) + powerwall_mock.get_sitemaster = Mock(return_value=sitemaster) + powerwall_mock.get_meters = Mock(return_value=meters) + powerwall_mock.get_grid_status = Mock(return_value=grid_status) + powerwall_mock.get_status = Mock(return_value=status) + powerwall_mock.get_device_type = Mock(return_value=device_type) + + return powerwall_mock + + +async def _mock_powerwall_site_name(hass, site_name): + powerwall_mock = MagicMock(Powerwall("1.2.3.4")) + + site_info_resp = SiteInfoResponse( + await _async_load_json_fixture(hass, "site_info.json") + ) + site_info_resp.site_name = site_name + powerwall_mock.get_site_info = Mock(return_value=site_info_resp) return powerwall_mock def _mock_powerwall_side_effect(site_info=None): - powerwall_mock = MagicMock() - type(powerwall_mock).site_info = PropertyMock(side_effect=site_info) + powerwall_mock = MagicMock(Powerwall("1.2.3.4")) + powerwall_mock.get_site_info = Mock(side_effect=site_info) return powerwall_mock diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index 621304793ab..e2af4cf05e9 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -15,10 +15,10 @@ async def test_sensors(hass): mock_powerwall = await _mock_powerwall_with_fixtures(hass) with patch( - "homeassistant.components.powerwall.config_flow.PowerWall", + "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.PowerWall", return_value=mock_powerwall, + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index f27d7e1f41b..a6752d838f3 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,13 +1,13 @@ """Test the Powerwall config flow.""" from asynctest import patch -from tesla_powerwall import PowerWallUnreachableError +from tesla_powerwall import PowerwallUnreachableError from homeassistant import config_entries, setup -from homeassistant.components.powerwall.const import DOMAIN, POWERWALL_SITE_NAME +from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from .mocks import _mock_powerwall_return_value, _mock_powerwall_side_effect +from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name async def test_form_source_user(hass): @@ -19,12 +19,10 @@ async def test_form_source_user(hass): assert result["type"] == "form" assert result["errors"] == {} - mock_powerwall = _mock_powerwall_return_value( - site_info={POWERWALL_SITE_NAME: "My site"} - ) + mock_powerwall = await _mock_powerwall_site_name(hass, "My site") with patch( - "homeassistant.components.powerwall.config_flow.PowerWall", + "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.async_setup", return_value=True @@ -47,12 +45,9 @@ async def test_form_source_import(hass): """Test we setup the config entry via import.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_powerwall = _mock_powerwall_return_value( - site_info={POWERWALL_SITE_NAME: "Imported site"} - ) - + mock_powerwall = await _mock_powerwall_site_name(hass, "Imported site") with patch( - "homeassistant.components.powerwall.config_flow.PowerWall", + "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.async_setup", return_value=True @@ -79,10 +74,10 @@ async def test_form_cannot_connect(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_powerwall = _mock_powerwall_side_effect(site_info=PowerWallUnreachableError) + mock_powerwall = _mock_powerwall_side_effect(site_info=PowerwallUnreachableError) with patch( - "homeassistant.components.powerwall.config_flow.PowerWall", + "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 3ab7e051a19..ce0c929ab92 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -15,10 +15,10 @@ async def test_sensors(hass): mock_powerwall = await _mock_powerwall_with_fixtures(hass) with patch( - "homeassistant.components.powerwall.config_flow.PowerWall", + "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.PowerWall", return_value=mock_powerwall + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() @@ -28,7 +28,7 @@ async def test_sensors(hass): identifiers={("powerwall", "Wom Energy_60Hz_240V_s_IEEE1547a_2014_13.5")}, connections=set(), ) - assert reg_device.model == "PowerWall 2 (hec)" + assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.45.1" assert reg_device.manufacturer == "Tesla" assert reg_device.name == "MySite" @@ -98,7 +98,7 @@ async def test_sensors(hass): assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_charge") - assert state.state == "47.32" + assert state.state == "47" expected_attributes = { "unit_of_measurement": UNIT_PERCENTAGE, "friendly_name": "Powerwall Charge",