Skip to content

Commit

Permalink
feat: add outputs status and command
Browse files Browse the repository at this point in the history
  • Loading branch information
xtimmy86x committed Nov 21, 2023
1 parent 6c4e308 commit daca0ed
Show file tree
Hide file tree
Showing 9 changed files with 725 additions and 29 deletions.
2 changes: 1 addition & 1 deletion custom_components/econnect_metronet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = ["alarm_control_panel", "binary_sensor", "sensor"]
PLATFORMS = ["alarm_control_panel", "binary_sensor", "sensor", "switch"]


async def async_migrate_entry(hass, config: ConfigEntry):
Expand Down
99 changes: 99 additions & 0 deletions custom_components/econnect_metronet/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self, connection, config=None):
self._last_ids = {
q.SECTORS: 0,
q.INPUTS: 0,
q.OUTPUTS: 0,
q.ALERTS: 0,
}

Expand Down Expand Up @@ -73,6 +74,21 @@ def inputs(self):
for input_id, item in self._inventory.get(q.INPUTS, {}).items():
yield input_id, item["name"]

@property
def outputs(self):
"""Iterate over the device's inventory of outputs.
This property provides an iterator over the device's inventory, where each item is a tuple
containing the output's ID and its name.
Yields:
tuple: A tuple where the first item is the output ID and the second item is the output name.
Example:
>>> device = AlarmDevice()
>>> list(device.outputs)
[(1, 'Output 1'), (2, 'Output 2')]
"""
for input_id, item in self._inventory.get(q.OUTPUTS, {}).items():
yield input_id, item["name"]

@property
def sectors(self):
"""Iterate over the device's inventory of sectors.
Expand Down Expand Up @@ -232,6 +248,7 @@ def update(self):
try:
sectors = self._connection.query(q.SECTORS)
inputs = self._connection.query(q.INPUTS)
outputs = self._connection.query(q.OUTPUTS)
alerts = self._connection.query(q.ALERTS)
except HTTPError as err:
_LOGGER.error(f"Device | Error during the update: {err.response.text}")
Expand All @@ -243,11 +260,13 @@ def update(self):
# Update the _inventory
self._inventory.update({q.SECTORS: sectors["sectors"]})
self._inventory.update({q.INPUTS: inputs["inputs"]})
self._inventory.update({q.OUTPUTS: outputs["outputs"]})
self._inventory.update({q.ALERTS: alerts["alerts"]})

# Update the _last_ids
self._last_ids[q.SECTORS] = sectors.get("last_id", 0)
self._last_ids[q.INPUTS] = inputs.get("last_id", 0)
self._last_ids[q.OUTPUTS] = outputs.get("last_id", 0)
self._last_ids[q.ALERTS] = alerts.get("last_id", 0)

# Update the internal state machine (mapping state)
Expand Down Expand Up @@ -284,3 +303,83 @@ def disarm(self, code, sectors=None):
except CodeError as err:
_LOGGER.error(f"Device | Credentials (alarm code) is incorrect: {err}")
raise err

def turn_off(self, outputs=None):
"""
Turn off a specified output.
Args:
outputs (str): The ID of the output to be turned off.
Raises:
HTTPError: If there is an error in the HTTP request to turn off the output.
Notes:
- The `element` is the sector ID used to arm/disarm the sector.
- This method checks for authentication requirements and control permissions
before attempting to turn off the output.
- If the output can't be manually controlled or if authentication is required but not provided,
appropriate error messages are logged.
Example:
To turn off an output with ID '1', use:
>>> device_instance.turn_off(outputs='1')
"""
for id, item in self.items(q.OUTPUTS):
if id == outputs:
if not item.get("control_denied_to_users"):
if item.get("do_not_require_authentication"):
element_id = item.get("element")
try:
self._connection.turn_off(element_id)
except HTTPError as err:
_LOGGER.error(f"Device | Error while turning off outputs: {err.response.text}")
raise err
else:
_LOGGER.error(
f"Device | Error while turning off output: {item.get('name')}, Required authentication"
)
else:
_LOGGER.error(
f"Device | Error while turning off output: {item.get('name')}, Can't be manual controlled"
)

def turn_on(self, outputs=None):
"""
Turn on a specified output.
Args:
outputs (str): The ID of the output to be turned on.
Raises:
HTTPError: If there is an error in the HTTP request to turn on the output.
Notes:
- The `element` is the sector ID used to arm/disarm the sector.
- This method checks for authentication requirements and control permissions
before attempting to turn on the output.
- If the output can't be manually controlled or if authentication is required but not provided,
appropriate error messages are logged.
Example:
To turn on an output with ID '1', use:
>>> device_instance.turn_on(outputs='1')
"""
for id, item in self.items(q.OUTPUTS):
if id == outputs:
if not item.get("control_denied_to_users"):
if item.get("do_not_require_authentication"):
element_id = item.get("element")
try:
self._connection.turn_on(element_id)
except HTTPError as err:
_LOGGER.error(f"Device | Error while turning on outputs: {err.response.text}")
raise err
else:
_LOGGER.error(
f"Device | Error while turning on output: {item.get('name')}, Required authentication"
)
else:
_LOGGER.error(
f"Device | Error while turning on output: {item.get('name')}, Can't be manual controlled"
)
95 changes: 95 additions & 0 deletions custom_components/econnect_metronet/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Module for e-connect switch (outputs)."""
from elmo import query as q
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from .const import (
CONF_EXPERIMENTAL,
CONF_FORCE_UPDATE,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
)
from .devices import AlarmDevice
from .helpers import generate_entity_id


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up e-connect binary sensors from a config entry."""
device = hass.data[DOMAIN][entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR]
# Load all entities and register outputs

outputs = []

# Iterate through the outputs of the provided device and create OutputSwitch objects
for output_id, name in device.outputs:
unique_id = f"{entry.entry_id}_{DOMAIN}_{q.OUTPUTS}_{output_id}"
outputs.append(OutputSwitch(unique_id, output_id, entry, name, coordinator, device))

async_add_entities(outputs)


class OutputSwitch(CoordinatorEntity, SwitchEntity):
"""Representation of a e-connect output switch."""

_attr_has_entity_name = True

def __init__(
self,
unique_id: str,
output_id: int,
config: ConfigEntry,
name: str,
coordinator: DataUpdateCoordinator,
device: AlarmDevice,
) -> None:
"""Construct."""
# Enable experimental settings from the configuration file
experimental = coordinator.hass.data[DOMAIN].get(CONF_EXPERIMENTAL, {})
self._attr_force_update = experimental.get(CONF_FORCE_UPDATE, False)

super().__init__(coordinator)
self.entity_id = generate_entity_id(config, name)
self._name = name
self._device = device
self._unique_id = unique_id
self._output_id = output_id

@property
def unique_id(self) -> str:
"""Return the unique identifier."""
return self._unique_id

@property
def name(self) -> str:
"""Return the name of this entity."""
return self._name

@property
def icon(self) -> str:
"""Return the icon used by this entity."""
return "hass:toggle-switch-variant"

@property
def is_on(self) -> bool:
"""Return the switch status (on/off)."""
return bool(self._device.get_status(q.OUTPUTS, self._output_id))

async def async_turn_off(self):
"""Turn the entity off."""
await self.hass.async_add_executor_job(self._device.turn_off, self._output_id)

async def async_turn_on(self):
"""Turn the entity off."""
await self.hass.async_add_executor_job(self._device.turn_on, self._output_id)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = [
"econnect-python==0.8.1",
"econnect-python @ git+https://github.com/palazzem/econnect-python@78b236efe81256af6da74fc5d6238b9ea3bfe220",
"homeassistant",
]

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def client(socket_enabled):
server.add(responses.POST, "https://example.com/api/strings", body=r.STRINGS, status=200)
server.add(responses.POST, "https://example.com/api/areas", body=r.AREAS, status=200)
server.add(responses.POST, "https://example.com/api/inputs", body=r.INPUTS, status=200)
server.add(responses.POST, "https://example.com/api/outputs", body=r.OUTPUTS, status=200)
server.add(responses.POST, "https://example.com/api/statusadv", body=r.STATUS, status=200)
yield client

Expand Down
84 changes: 81 additions & 3 deletions tests/fixtures/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_client_get_sectors_status(server):
"Successful": true
}
]"""
STRINGS = """[
STRINGS = r"""[
{
"AccountId": 1,
"Class": 9,
Expand Down Expand Up @@ -136,12 +136,44 @@ def test_client_get_sectors_status(server):
"Version": "AAAAAAAAgRw="
},
{
"AccountId": 3,
"AccountId": 1,
"Class": 10,
"Index": 3,
"Description": "Outdoor Sensor 3",
"Created": "/Date(1546004147493+0100)/",
"Version": "AAAAAAAAgRw="
},
{
"AccountId": 1,
"Class": 12,
"Index": 0,
"Description": "Output 1",
"Created": "\/Date(1699548985673+0100)\/",
"Version": "AAAAAAAceCo="
},
{
"AccountId": 1,
"Class": 12,
"Index": 1,
"Description": "Output 2",
"Created": "\/Date(1699548985673+0100)\/",
"Version": "AAAAAAAceCs="
},
{
"AccountId": 1,
"Class": 12,
"Index": 2,
"Description": "Output 3",
"Created": "\/Date(1699548985673+0100)\/",
"Version": "AAAAAAAceCw="
},
{
"AccountId": 1,
"Class": 12,
"Index": 3,
"Description": "Output 4",
"Created": "\/Date(1699548985673+0100)\/",
"Version": "AAAAAAAceC0="
}
]"""
AREAS = """[
Expand Down Expand Up @@ -175,7 +207,7 @@ def test_client_get_sectors_status(server):
"Active": false,
"ActivePartial": false,
"Max": false,
"Activable": true,
"Activable": false,
"ActivablePartial": false,
"InUse": true,
"Id": 3,
Expand Down Expand Up @@ -248,6 +280,52 @@ def test_client_get_sectors_status(server):
"InProgress": false
}
]"""
OUTPUTS = """[
{
"Active": true,
"InUse": true,
"DoNotRequireAuthentication": true,
"ControlDeniedToUsers": false,
"Id": 1,
"Index": 0,
"Element": 1,
"CommandId": 0,
"InProgress": false
},
{
"Active": true,
"InUse": true,
"DoNotRequireAuthentication": false,
"ControlDeniedToUsers": false,
"Id": 2,
"Index": 1,
"Element": 2,
"CommandId": 0,
"InProgress": false
},
{
"Active": false,
"InUse": true,
"DoNotRequireAuthentication": false,
"ControlDeniedToUsers": true,
"Id": 3,
"Index": 2,
"Element": 3,
"CommandId": 0,
"InProgress": false
},
{
"Active": false,
"InUse": false,
"DoNotRequireAuthentication": false,
"ControlDeniedToUsers": false,
"Id": 4,
"Index": 3,
"Element": 4,
"CommandId": 0,
"InProgress": false
}
]"""
STATUS = """
{
"StatusUid": 1,
Expand Down
Loading

0 comments on commit daca0ed

Please sign in to comment.