From 9d1cf68b9b3a1a83489c2ba08a904481445498b2 Mon Sep 17 00:00:00 2001 From: davesmeghead Date: Sat, 10 Aug 2024 12:10:16 +0100 Subject: [PATCH] 0.9.6.21 - Added X10 Home Assistant Service (Action) 0.9.6.21 - Added X10 Home Assistant Service (Action) Although the X10 devices are Switches in Home Assistant, and provide a simple On/Off control in the Frontend, it is now possible to create a service (action) call to change the X10 device state as Off, On, Dimmer, Brighten. These last two have no feedback on how dim/bright the device is and so it cannot be made a Light Entity in Home Assistant. --- custom_components/visonic/__init__.py | 32 ++++++++++++++- custom_components/visonic/client.py | 41 +++++++++++++++++-- custom_components/visonic/const.py | 3 ++ custom_components/visonic/manifest.json | 2 +- custom_components/visonic/pyconst.py | 2 +- custom_components/visonic/pyhelper.py | 2 +- custom_components/visonic/pyvisonic.py | 4 +- custom_components/visonic/services.yaml | 22 ++++++++++ custom_components/visonic/switch.py | 2 +- .../visonic/translations/en.json | 22 ++++++++++ 10 files changed, 121 insertions(+), 11 deletions(-) diff --git a/custom_components/visonic/__init__.py b/custom_components/visonic/__init__.py index b14d03b..089bda7 100644 --- a/custom_components/visonic/__init__.py +++ b/custom_components/visonic/__init__.py @@ -24,13 +24,14 @@ SERVICE_RELOAD, ) -from .pyconst import AlPanelCommand +from .pyconst import AlPanelCommand, AlX10Command from .client import VisonicClient from .const import ( DOMAIN, ALARM_PANEL_EVENTLOG, ALARM_PANEL_RECONNECT, ALARM_PANEL_COMMAND, + ALARM_PANEL_X10, ALARM_SENSOR_BYPASS, ALARM_SENSOR_IMAGE, ATTR_BYPASS, @@ -45,6 +46,7 @@ CONF_EMULATION_MODE, CONF_SENSOR_EVENTS, CONF_COMMAND, + CONF_X10_COMMAND, available_emulation_modes, AvailableNotifications ) @@ -53,7 +55,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) -# the 5 schemas for the HA service calls +# the 6 schemas for the HA service calls ALARM_SCHEMA_EVENTLOG = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, @@ -69,6 +71,13 @@ } ) +ALARM_SCHEMA_X10 = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_X10_COMMAND) : vol.In([x.lower().replace("_"," ").title() for x in list(AlX10Command.get_variables().keys())]), + } +) + ALARM_SCHEMA_RECONNECT = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, @@ -187,6 +196,17 @@ async def service_panel_command(call): sendHANotification(f"Service Panel command failed - Panel {panel} not found") else: sendHANotification(f"Service Panel command failed - Panel not found") + + async def service_panel_x10(call): + """Handler for panel command service""" + _LOGGER.info(f"Service Panel x10 called") + client, panel = getClient(call) + if client is not None: + await client.service_panel_x10(call) + elif panel is not None: + sendHANotification(f"Service Panel x10 failed - Panel {panel} not found") + else: + sendHANotification(f"Service Panel x10 failed - Panel not found") async def service_sensor_bypass(call): """Handler for sensor bypass service""" @@ -242,12 +262,20 @@ async def handle_reload(call) -> None: service_panel_command, schema=ALARM_SCHEMA_COMMAND, ) + hass.services.async_register( + DOMAIN, + ALARM_PANEL_X10, + service_panel_x10, + schema=ALARM_SCHEMA_X10, + ) hass.services.async_register( DOMAIN, ALARM_SENSOR_BYPASS, service_sensor_bypass, schema=ALARM_SCHEMA_BYPASS, ) + + # hass.services.async_register( # DOMAIN, # ALARM_SENSOR_IMAGE, diff --git a/custom_components/visonic/client.py b/custom_components/visonic/client.py index 6145a2c..e461478 100644 --- a/custom_components/visonic/client.py +++ b/custom_components/visonic/client.py @@ -96,6 +96,7 @@ CONF_RETRY_CONNECTION_COUNT, CONF_RETRY_CONNECTION_DELAY, CONF_COMMAND, + CONF_X10_COMMAND, DOMAIN, NOTIFICATION_ID, NOTIFICATION_TITLE, @@ -122,7 +123,7 @@ # "trigger", #] -CLIENT_VERSION = "0.9.6.20" +CLIENT_VERSION = "0.9.6.21" MAX_CLIENT_LOG_ENTRIES = 300 @@ -913,7 +914,7 @@ def getConfigData(self) -> PanelConfig: async def _checkUserPermission(self, call, perm, entity): user = await self.hass.auth.async_get_user(call.context.user_id) - self.logstate_debug(f"User check {call.context.user_id=} user={user=}") + #self.logstate_debug(f"User check {call.context.user_id=} user={user=}") if user is None: raise UnknownUser( @@ -1051,7 +1052,7 @@ def _generateBusEventReason(self, event_id: PanelCondition, reason: AlCommandSta def setX10(self, ident: int, state: AlX10Command): """Send an X10 command to the panel.""" if not self.DisableAllCommands: - # ident in range 0 to 15, state can be one of "off", "on", "dim", "brighten" + # ident in range 0 to 15, state can be one of "off", "on", "dimmer", "brighten" if self.visonicProtocol is not None: retval = self.visonicProtocol.setX10(ident, state) self._generateBusEventReason(PanelCondition.CHECK_X10_COMMAND, retval, "X10", "Send X10 Command") @@ -1259,6 +1260,19 @@ def sendBypass(self, devid: int, bypass: bool, code: str) -> AlCommandStatus: self.createNotification(AvailableNotifications.COMMAND_NOT_SENT, f"Visonic Alarm Panel: Panel Commands Disabled") return AlCommandStatus.FAIL_USER_CONFIG_PREVENTED + def sendX10(self, devid: int, command : AlX10Command) -> AlCommandStatus: + """Send the x10 command to the panel.""" + if not self.DisableAllCommands: + if self.visonicProtocol is not None: + retval = self.visonicProtocol.setX10(devid, command) + else: + retval = AlCommandStatus.FAIL_PANEL_NO_CONNECTION + self._generateBusEventReason(PanelCondition.CHECK_X10_COMMAND, retval, "X10", "X10 Request") + return retval + else: + self.createNotification(AvailableNotifications.COMMAND_NOT_SENT, f"Visonic Alarm Panel: Panel Commands Disabled") + return AlCommandStatus.FAIL_USER_CONFIG_PREVENTED + async def service_sensor_bypass(self, call): """Service call to bypass a sensor in the panel.""" if await self.check_the_basics(call, "sensor bypass"): @@ -1333,6 +1347,27 @@ async def service_panel_command(self, call): except Exception as ex: self.logstate_warning(f"Not making command request. Exception {ex}") + def sendX10Command(self, devid: int, command : AlX10Command): + """Send a request to set the X10 device """ + if not self.DisableAllCommands: + self.sendX10(devid, command) + else: + self.createNotification(AvailableNotifications.COMMAND_NOT_SENT, f"Visonic Alarm Panel: Panel Commands Disabled") + + async def service_panel_x10(self, call): + """Service call to set an x10 device in the panel.""" + if await self.check_the_basics(call, "x10 command"): + devid, eid = await self.decode_entity(call, Platform.SWITCH, "x10 switch command", AvailableNotifications.X10_PROBLEM) # ************************************************************************************************ + if devid is not None and devid >= 1 and devid <= 16: + if CONF_X10_COMMAND in call.data: + command = call.data[CONF_X10_COMMAND] + command_x = AlX10Command.value_of(command.upper()); + self.logstate_debug(f" X10 Command {command} {command_x}") + self.sendX10Command(devid, command_x) + else: + self.createNotification(AvailableNotifications.COMMAND_NOT_SENT, f"Attempt to set X10 device for panel {self.getPanelID()}, command not set for entity {eid}") + else: + self.createNotification(AvailableNotifications.X10_PROBLEM, f"Attempt to set X10 device for panel {self.getPanelID()}, incorrect device {devid} for entity {eid}") # ======================================================================================================= # ======================================================================================================= diff --git a/custom_components/visonic/const.py b/custom_components/visonic/const.py index 2cd947c..f3f21d0 100644 --- a/custom_components/visonic/const.py +++ b/custom_components/visonic/const.py @@ -37,6 +37,7 @@ class SensorEntityFeature(IntFlag): # The HA Services. These strings match the content of the services.yaml file ALARM_PANEL_COMMAND = "alarm_panel_command" +ALARM_PANEL_X10 = "alarm_panel_x10" ALARM_PANEL_EVENTLOG = "alarm_panel_eventlog" ALARM_PANEL_RECONNECT = "alarm_panel_reconnect" ALARM_SENSOR_BYPASS = "alarm_sensor_bypass" @@ -82,6 +83,7 @@ class SensorEntityFeature(IntFlag): CONF_LANGUAGE = "language" CONF_EMULATION_MODE = "emulation_mode" CONF_COMMAND = "command" +CONF_X10_COMMAND = "x10command" # settings than can be modified CONF_ENABLE_REMOTE_ARM = "allow_remote_arm" @@ -116,6 +118,7 @@ class AvailableNotifications(str, Enum): IMAGE_PROBLEM = 'image_problem' EVENTLOG_PROBLEM = 'eventlog_problem' COMMAND_NOT_SENT = 'command_not_sent' + X10_PROBLEM = 'x10_problem' available_emulation_modes = [ "Powerlink Emulation", diff --git a/custom_components/visonic/manifest.json b/custom_components/visonic/manifest.json index e9dd819..76481bd 100644 --- a/custom_components/visonic/manifest.json +++ b/custom_components/visonic/manifest.json @@ -11,5 +11,5 @@ "loggers": ["visonic"], "requirements": ["Pillow", "pyserial_asyncio"], "single_config_entry": false, - "version": "0.9.6.20" + "version": "0.9.6.21" } diff --git a/custom_components/visonic/pyconst.py b/custom_components/visonic/pyconst.py index 4c5734a..26113e4 100644 --- a/custom_components/visonic/pyconst.py +++ b/custom_components/visonic/pyconst.py @@ -204,7 +204,7 @@ class AlPanelCommand(AlEnum): class AlX10Command(AlEnum): OFF = AlIntEnum(0) ON = AlIntEnum(1) - DIM = AlIntEnum(2) + DIMMER = AlIntEnum(2) BRIGHTEN = AlIntEnum(3) a = AlX10Command() diff --git a/custom_components/visonic/pyhelper.py b/custom_components/visonic/pyhelper.py index 8117dcd..fbd618c 100644 --- a/custom_components/visonic/pyhelper.py +++ b/custom_components/visonic/pyhelper.py @@ -458,7 +458,7 @@ def fromJSON(self, decode): self.location = titlecase(decode["location"]) if "state" in decode: s = AlX10Command.value_of(decode["state"].upper()) - self.state = (s == AlX10Command.ON or s == AlX10Command.BRIGHTEN or s == AlX10Command.DIM) + self.state = (s == AlX10Command.ON or s == AlX10Command.BRIGHTEN or s == AlX10Command.DIMMER) def toJSON(self) -> dict: dd=json.dumps({ diff --git a/custom_components/visonic/pyvisonic.py b/custom_components/visonic/pyvisonic.py index 87edbc1..14a4f30 100644 --- a/custom_components/visonic/pyvisonic.py +++ b/custom_components/visonic/pyvisonic.py @@ -100,7 +100,7 @@ def convertByteArray(s) -> bytearray: from pyhelper import (toString, MyChecksumCalc, AlImageManager, ImageRecord, titlecase, pmPanelTroubleType_t, pmPanelAlarmType_t, AlPanelInterfaceHelper, AlSensorDeviceHelper, AlSwitchDeviceHelper) -PLUGIN_VERSION = "1.3.6.5" +PLUGIN_VERSION = "1.3.6.6" # Some constants to help readability of the code @@ -346,7 +346,7 @@ def convertByteArray(s) -> bytearray: # Data to embed in the MSG_X10PGM message pmX10State_t = { - AlX10Command.OFF : 0x00, AlX10Command.ON : 0x01, AlX10Command.DIM : 0x0A, AlX10Command.BRIGHTEN : 0x0B + AlX10Command.OFF : 0x00, AlX10Command.ON : 0x01, AlX10Command.DIMMER : 0x0A, AlX10Command.BRIGHTEN : 0x0B } ############################################################################################################################################################################################################################################## diff --git a/custom_components/visonic/services.yaml b/custom_components/visonic/services.yaml index b86bae1..c23ca04 100644 --- a/custom_components/visonic/services.yaml +++ b/custom_components/visonic/services.yaml @@ -55,6 +55,28 @@ alarm_panel_reconnect: integration: visonic domain: alarm_control_panel + +alarm_panel_x10: + fields: + entity_id: + required: true + example: 'switch.visonic_x02' + selector: + entity: + integration: visonic + domain: switch + x10command: + required: true + example: "Off" + default: "Off" + selector: + select: + options: + - "Off" + - "On" + - "Dimmer" + - "Brighten" + alarm_sensor_bypass: fields: entity_id: diff --git a/custom_components/visonic/switch.py b/custom_components/visonic/switch.py index 54e88ae..a13135a 100644 --- a/custom_components/visonic/switch.py +++ b/custom_components/visonic/switch.py @@ -135,7 +135,7 @@ def device_info(self): "manufacturer": "Visonic", } - # "off" "on" "dim" "brighten" + # "off" "on" "dimmer" "brighten" def turnmeonandoff(self, state : AlX10Command): """Send disarm command.""" self._client.setX10(self._x10id, state) diff --git a/custom_components/visonic/translations/en.json b/custom_components/visonic/translations/en.json index 086d810..f75d66c 100644 --- a/custom_components/visonic/translations/en.json +++ b/custom_components/visonic/translations/en.json @@ -190,6 +190,20 @@ } } }, + "alarm_panel_x10": { + "name": "Command X10", + "description": "Send an X10 Command to an X10 Device.", + "fields": { + "entity_id": { + "name": "Visonic X10 Switch", + "description": "Name of the visonic x10 switch. This is case sensitive and a mandatory setting. The 'switch.' text is optional." + }, + "x10command": { + "name": "X10 Command", + "description": "X10 Command: Off, On, Dimmer, Brighten." + } + } + }, "alarm_panel_reconnect": { "name": "Panel Reconnection", "description": "Reconnect to the Alarm Panel following a previous problem.", @@ -239,6 +253,14 @@ "arm_away_instant": "Arm Away Instant" } }, + "x10command": { + "options": { + "Off": "Off", + "On": "On", + "Dimmer": "Decrease Brightness", + "Brighten": "Increase Brightness" + } + }, "siren_sounding": { "options": { "intruder": "Intruder",