-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add send_command action / service * Re-create the service, send_command Sem-Ver: feature
- Loading branch information
1 parent
3b7be4a
commit 7118346
Showing
9 changed files
with
243 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import asyncio | ||
import logging | ||
from typing import Final | ||
|
||
import homeassistant.helpers.config_validation as cv | ||
import voluptuous as vol | ||
from homeassistant.const import CONF_USERNAME | ||
from homeassistant.core import HomeAssistant, ServiceCall | ||
from homeassistant.helpers import entity_registry as er | ||
from homeassistant.helpers.service import verify_domain_control | ||
|
||
from .bridge import HubspaceBridge | ||
from .const import DOMAIN | ||
|
||
SERVICE_SEND_COMMAND = "send_command" | ||
|
||
SERVICE_SEND_COMMAND_FUNC_CLASS: Final[str] = "function_class" | ||
SERVICE_SEND_COMMAND_FUNC_INSTANCE: Final[str] = "function_instance" | ||
SERVICE_SEND_COMMAND_VALUE: Final[str] = "value" | ||
SERVICE_SEND_COMMAND_ACCOUNT: Final[str] = "account" | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
def async_register_services(hass: HomeAssistant) -> None: | ||
"""Register services for Hubspace integration.""" | ||
|
||
async def send_command(call: ServiceCall, skip_reload=True) -> None: | ||
states: list[dict] = [] | ||
states.append( | ||
{ | ||
"value": call.data.get(SERVICE_SEND_COMMAND_VALUE), | ||
"functionClass": call.data.get(SERVICE_SEND_COMMAND_FUNC_CLASS), | ||
"functionInstance": call.data.get(SERVICE_SEND_COMMAND_FUNC_INSTANCE), | ||
} | ||
) | ||
entity_reg = er.async_get(hass) | ||
tasks = [] | ||
account = call.data.get("account") | ||
for entity_name in call.data.get("entity_id", []): | ||
entity = entity_reg.async_get(entity_name) | ||
bridge = await find_bridge(hass, account) | ||
if bridge: | ||
tasks.append(bridge.api.send_service_request(entity.unique_id, states)) | ||
else: | ||
LOGGER.warning("No bridge using account %s", account) | ||
return | ||
await asyncio.gather(*tasks) | ||
|
||
def optional(value): | ||
if value is None: | ||
return value | ||
return cv.string(value) | ||
|
||
if not hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): | ||
hass.services.async_register( | ||
DOMAIN, | ||
SERVICE_SEND_COMMAND, | ||
verify_domain_control(hass, DOMAIN)(send_command), | ||
schema=vol.Schema( | ||
{ | ||
vol.Required("entity_id"): cv.entity_ids, | ||
vol.Required(SERVICE_SEND_COMMAND_FUNC_CLASS): cv.string, | ||
vol.Required(SERVICE_SEND_COMMAND_VALUE): cv.string, | ||
vol.Optional(SERVICE_SEND_COMMAND_FUNC_INSTANCE): optional, | ||
vol.Optional(SERVICE_SEND_COMMAND_ACCOUNT): optional, | ||
} | ||
), | ||
) | ||
|
||
|
||
async def find_bridge(hass: HomeAssistant, username: str) -> HubspaceBridge | None: | ||
"""Find the bridge for the given username""" | ||
for bridge in hass.data[DOMAIN].values(): | ||
if username is None: | ||
return bridge | ||
if bridge.config_entry.data[CONF_USERNAME] == username: | ||
return bridge | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,18 @@ send_command: | |
target: | ||
entity: | ||
integration: hubspace | ||
domain: light | ||
domain: | ||
- fan | ||
- light | ||
- lock | ||
- switch | ||
- valve | ||
fields: | ||
account: | ||
name: account | ||
description: Username associated with the device. If not present, it will use the first Hubspace instance | ||
required: false | ||
example: [email protected] | ||
value: | ||
name: value | ||
description: value you want to send | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import pytest | ||
import voluptuous | ||
from aiohubspace.v1.device import HubspaceState | ||
|
||
from custom_components.hubspace import const, services | ||
|
||
from .utils import create_devices_from_data, modify_state | ||
|
||
fan_zandra = create_devices_from_data("fan-ZandraFan.json") | ||
fan_zandra_light = fan_zandra[1] | ||
fan_zandra_light_id = "light.friendly_device_2_light" | ||
|
||
|
||
@pytest.fixture | ||
async def mocked_entity(mocked_entry): | ||
hass, entry, bridge = mocked_entry | ||
await bridge.lights.initialize_elem(fan_zandra_light) | ||
await bridge.devices.initialize_elem(fan_zandra[2]) | ||
bridge.add_device(fan_zandra_light.id, bridge.lights) | ||
bridge.add_device(fan_zandra[2].id, bridge.devices) | ||
bridge.lights._initialized = True | ||
bridge.devices._initialized = True | ||
await hass.config_entries.async_setup(entry.entry_id) | ||
await hass.async_block_till_done() | ||
yield hass, entry, bridge | ||
await bridge.close() | ||
|
||
|
||
@pytest.mark.asyncio | ||
@pytest.mark.parametrize( | ||
"account, entity_id, error_entity, error_bridge", | ||
[ | ||
# Use any bridge | ||
( | ||
None, | ||
fan_zandra_light_id, | ||
None, | ||
None, | ||
), | ||
# Use bridge that has an account match | ||
("username", fan_zandra_light_id, None, None), | ||
# Invalid entity | ||
("username", "i dont exist", True, None), | ||
# No bridge that uses username | ||
("username2", fan_zandra_light_id, None, True), | ||
], | ||
) | ||
async def test_service_valid_no_username( | ||
account, entity_id, error_entity, error_bridge, mocked_entity, caplog | ||
): | ||
hass, entry, bridge = mocked_entity | ||
assert hass.states.get(fan_zandra_light_id).state == "on" | ||
if not error_entity and not error_bridge: | ||
await hass.services.async_call( | ||
const.DOMAIN, | ||
services.SERVICE_SEND_COMMAND, | ||
service_data={ | ||
"entity_id": [entity_id], | ||
"value": "off", | ||
"function_class": "power", | ||
"function_instance": "light-power", | ||
"account": account, | ||
}, | ||
blocking=True, | ||
) | ||
await hass.async_block_till_done() | ||
update_call = bridge.request.call_args_list[-1] | ||
assert update_call.args[0] == "put" | ||
payload = update_call.kwargs["json"] | ||
assert payload["metadeviceId"] == fan_zandra_light.id | ||
assert payload["values"] == [ | ||
{ | ||
"functionClass": "power", | ||
"functionInstance": "light-power", | ||
"value": "off", | ||
} | ||
] | ||
# Now generate update event by emitting the json we've sent as incoming event | ||
light_update = create_devices_from_data("fan-ZandraFan.json")[1] | ||
modify_state( | ||
light_update, | ||
HubspaceState( | ||
functionClass="power", | ||
functionInstance="light-power", | ||
value="off", | ||
), | ||
) | ||
event = { | ||
"type": "update", | ||
"device_id": light_update.id, | ||
"device": light_update, | ||
"force_forward": True, | ||
} | ||
bridge.emit_event("update", event) | ||
await hass.async_block_till_done() | ||
assert hass.states.get(fan_zandra_light_id).state == "off" | ||
else: | ||
bridge.request.assert_not_called() | ||
if error_entity: | ||
with pytest.raises(voluptuous.error.MultipleInvalid): | ||
await hass.services.async_call( | ||
const.DOMAIN, | ||
services.SERVICE_SEND_COMMAND, | ||
service_data={ | ||
"entity_id": [entity_id], | ||
"value": "off", | ||
"function_class": "power", | ||
"function_instance": "light-power", | ||
"account": account, | ||
}, | ||
blocking=True, | ||
) | ||
await hass.async_block_till_done() | ||
else: | ||
await hass.services.async_call( | ||
const.DOMAIN, | ||
services.SERVICE_SEND_COMMAND, | ||
service_data={ | ||
"entity_id": [entity_id], | ||
"value": "off", | ||
"function_class": "power", | ||
"function_instance": "light-power", | ||
"account": account, | ||
}, | ||
blocking=True, | ||
) | ||
await hass.async_block_till_done() | ||
if error_bridge: | ||
assert f"No bridge using account {account}" in caplog.text |