diff --git a/custom_components/openevse/services.py b/custom_components/openevse/services.py index 8b7a9e2..2ad4bc8 100644 --- a/custom_components/openevse/services.py +++ b/custom_components/openevse/services.py @@ -11,6 +11,7 @@ SupportsResponse, callback, ) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers import device_registry as dr from .const import ( @@ -60,7 +61,7 @@ def async_register(self) -> None: self._set_override, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_STATE): vol.Coerce(str), vol.Optional(ATTR_CHARGE_CURRENT): vol.All( vol.Coerce(int), vol.Range(min=1, max=48) @@ -85,7 +86,7 @@ def async_register(self) -> None: self._set_limit, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Required(ATTR_TYPE): vol.Coerce(str), vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Optional(ATTR_AUTO_RELEASE): vol.Coerce(bool), @@ -99,7 +100,7 @@ def async_register(self) -> None: self._clear_override, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), ) @@ -110,7 +111,7 @@ def async_register(self) -> None: self._clear_limit, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), ) @@ -121,7 +122,7 @@ def async_register(self) -> None: self._get_limit, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), supports_response=SupportsResponse.ONLY, @@ -133,7 +134,7 @@ def async_register(self) -> None: self._make_claim, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_STATE): vol.Coerce(str), vol.Optional(ATTR_CHARGE_CURRENT): vol.All( vol.Coerce(int), vol.Range(min=1, max=48) @@ -152,7 +153,7 @@ def async_register(self) -> None: self._list_claims, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), supports_response=SupportsResponse.ONLY, @@ -164,7 +165,7 @@ def async_register(self) -> None: self._release_claim, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), ) @@ -175,7 +176,7 @@ def async_register(self) -> None: self._list_overrides, schema=vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.Coerce(str), + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ), supports_response=SupportsResponse.ONLY, @@ -185,7 +186,7 @@ def async_register(self) -> None: async def _set_override(self, service: ServiceCall) -> None: """Set the override.""" data = service.data - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -239,7 +240,7 @@ async def _clear_override(self, service: ServiceCall) -> None: """Clear the manual override.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -260,7 +261,7 @@ async def _clear_override(self, service: ServiceCall) -> None: async def _set_limit(self, service: ServiceCall) -> None: """Set the limit.""" data = service.data - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -294,7 +295,7 @@ async def _clear_limit(self, service: ServiceCall) -> None: """Clear the limit.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -316,7 +317,7 @@ async def _get_limit(self, service: ServiceCall) -> ServiceResponse: """Get the limit.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -338,7 +339,7 @@ async def _get_limit(self, service: ServiceCall) -> ServiceResponse: async def _make_claim(self, service: ServiceCall) -> None: """Make a claim.""" data = service.data - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -382,7 +383,7 @@ async def _release_claim(self, service: ServiceCall) -> None: """Release a claim.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -404,7 +405,7 @@ async def _list_claims(self, service: ServiceCall) -> ServiceResponse: """Get the claims.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) @@ -433,7 +434,7 @@ async def _list_overrides(self, service: ServiceCall) -> ServiceResponse: """Get the overrides.""" data = service.data _LOGGER.debug("Data: %s", data) - for device in data.values(): + for device in data[ATTR_DEVICE_ID]: device_id = device _LOGGER.debug("Device ID: %s", device_id) diff --git a/tests/test_services.py b/tests/test_services.py index dda3451..dc5fb01 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -9,16 +9,23 @@ from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import CONF_DEVICE_ID from homeassistant.helpers import entity_registry as er from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.openevse.const import ( + ATTR_DEVICE_ID, + ATTR_STATE, + ATTR_TYPE, + ATTR_VALUE, DOMAIN, + SERVICE_CLEAR_LIMIT, SERVICE_CLEAR_OVERRIDE, SERVICE_GET_LIMIT, + SERVICE_SET_LIMIT, + SERVICE_SET_OVERRIDE, SERVICE_LIST_CLAIMS, SERVICE_LIST_OVERRIDES, + SERVICE_MAKE_CLAIM, SERVICE_RELEASE_CLAIM, ) @@ -65,7 +72,7 @@ async def test_list_claims( response = await hass.services.async_call( DOMAIN, SERVICE_LIST_CLAIMS, - {CONF_DEVICE_ID: entry.device_id}, + {ATTR_DEVICE_ID: entry.device_id}, blocking=True, return_response=True, ) @@ -86,7 +93,7 @@ async def test_list_claims( } -async def test_list_overrides( +async def test_make_claim( hass, test_charger, mock_aioclient, @@ -94,24 +101,16 @@ async def test_list_overrides( entity_registry: er.EntityRegistry, caplog, ): - """Test setup_entry.""" + """Test release claim service call.""" entry = MockConfigEntry( domain=DOMAIN, title=CHARGER_NAME, data=CONFIG_DATA, ) - value = { - "state": "active", - "charge_current": 0, - "max_current": 0, - "energy_limit": 0, - "time_limit": 0, - "auto_release": True, - } - mock_aioclient.get( - TEST_URL_OVERRIDE, + mock_aioclient.post( + f"{TEST_URL_CLAIMS}/4", status=200, - body=json.dumps(value), + body='[{"msg":"done"}]', repeat=True, ) entry.add_to_hass(hass) @@ -124,14 +123,55 @@ async def test_list_overrides( # setup service call with caplog.at_level(logging.DEBUG): - response = await hass.services.async_call( + await hass.services.async_call( DOMAIN, - SERVICE_LIST_OVERRIDES, - {CONF_DEVICE_ID: entry.device_id}, + SERVICE_MAKE_CLAIM, + { + ATTR_DEVICE_ID: entry.device_id, + ATTR_STATE: "active", + }, blocking=True, - return_response=True, ) - assert response == value + assert "Make claim response:" in caplog.text + + +async def test_release_claim( + hass, + test_charger, + mock_aioclient, + mock_ws_start, + entity_registry: er.EntityRegistry, + caplog, +): + """Test release claim service call.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CHARGER_NAME, + data=CONFIG_DATA, + ) + mock_aioclient.delete( + f"{TEST_URL_CLAIMS}/4", + status=200, + body='[{"msg":"done"}]', + repeat=True, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry = entity_registry.async_get("sensor.openevse_station_status") + assert entry + assert entry.device_id + + # setup service call + with caplog.at_level(logging.DEBUG): + await hass.services.async_call( + DOMAIN, + SERVICE_RELEASE_CLAIM, + {ATTR_DEVICE_ID: entry.device_id}, + blocking=True, + ) + assert "Release claim command sent." in caplog.text async def test_get_limit( @@ -166,14 +206,14 @@ async def test_get_limit( response = await hass.services.async_call( DOMAIN, SERVICE_GET_LIMIT, - {CONF_DEVICE_ID: entry.device_id}, + {ATTR_DEVICE_ID: entry.device_id}, blocking=True, return_response=True, ) assert response == {"type": "energy", "value": 10} -async def test_release_claim( +async def test_clear_limit( hass, test_charger, mock_aioclient, @@ -181,16 +221,54 @@ async def test_release_claim( entity_registry: er.EntityRegistry, caplog, ): - """Test release claim service call.""" + """Test setup_entry.""" entry = MockConfigEntry( domain=DOMAIN, title=CHARGER_NAME, data=CONFIG_DATA, ) mock_aioclient.delete( - f"{TEST_URL_CLAIMS}/4", + TEST_URL_LIMIT, status=200, - body='[{"msg":"done"}]', + body='{"msg": "Deleted"}', + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry = entity_registry.async_get("sensor.openevse_station_status") + assert entry + assert entry.device_id + + # setup service call + with caplog.at_level(logging.DEBUG): + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_LIMIT, + {ATTR_DEVICE_ID: entry.device_id}, + blocking=True, + ) + assert "Limit clear command sent." in caplog.text + + +async def test_set_limit( + hass, + test_charger, + mock_aioclient, + mock_ws_start, + entity_registry: er.EntityRegistry, + caplog, +): + """Test setup_entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CHARGER_NAME, + data=CONFIG_DATA, + ) + mock_aioclient.post( + TEST_URL_LIMIT, + status=200, + body='{"msg": "OK"}', repeat=True, ) entry.add_to_hass(hass) @@ -205,11 +283,15 @@ async def test_release_claim( with caplog.at_level(logging.DEBUG): await hass.services.async_call( DOMAIN, - SERVICE_RELEASE_CLAIM, - {CONF_DEVICE_ID: entry.device_id}, + SERVICE_SET_LIMIT, + { + ATTR_DEVICE_ID: entry.device_id, + ATTR_TYPE: "range", + ATTR_VALUE: "50", + }, blocking=True, ) - assert "Release claim command sent." in caplog.text + assert "Set Limit response:" in caplog.text async def test_clear_override( @@ -244,7 +326,96 @@ async def test_clear_override( await hass.services.async_call( DOMAIN, SERVICE_CLEAR_OVERRIDE, - {CONF_DEVICE_ID: entry.device_id}, + {ATTR_DEVICE_ID: entry.device_id}, blocking=True, ) assert "Override clear command sent." in caplog.text + + +async def test_list_overrides( + hass, + test_charger, + mock_aioclient, + mock_ws_start, + entity_registry: er.EntityRegistry, + caplog, +): + """Test setup_entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CHARGER_NAME, + data=CONFIG_DATA, + ) + value = { + "state": "active", + "charge_current": 0, + "max_current": 0, + "energy_limit": 0, + "time_limit": 0, + "auto_release": True, + } + mock_aioclient.get( + TEST_URL_OVERRIDE, + status=200, + body=json.dumps(value), + repeat=True, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry = entity_registry.async_get("sensor.openevse_station_status") + assert entry + assert entry.device_id + + # setup service call + with caplog.at_level(logging.DEBUG): + response = await hass.services.async_call( + DOMAIN, + SERVICE_LIST_OVERRIDES, + {ATTR_DEVICE_ID: entry.device_id}, + blocking=True, + return_response=True, + ) + assert response == value + + +async def test_set_override( + hass, + test_charger, + mock_aioclient, + mock_ws_start, + entity_registry: er.EntityRegistry, + caplog, +): + """Test release claim service call.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CHARGER_NAME, + data=CONFIG_DATA, + ) + mock_aioclient.post( + TEST_URL_OVERRIDE, + status=200, + body='{"msg": "OK"}', + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry = entity_registry.async_get("sensor.openevse_station_status") + assert entry + assert entry.device_id + + # setup service call + with caplog.at_level(logging.DEBUG): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OVERRIDE, + { + ATTR_DEVICE_ID: entry.device_id, + ATTR_STATE: "disabled", + }, + blocking=True, + ) + assert "Set Override response:" in caplog.text