Skip to content

Commit

Permalink
Development Release 0.9.1.0
Browse files Browse the repository at this point in the history
This is the first development release.
I have made significant changes to the code since last years release, some parts have been re-written completely.

These changes have been made to:
- Make it more robust
- Get ready for additional features coming over the next few months, particularly for PowerMaster panels

This current dev release includes:
- Changes to the Home Assistant Service calls
- Changes to the Home Assistant Events and the Event Data

Specifically for PowerMaster Panels, the changes in this dev release include:
- The "Command" Service call includes Trigger, Mute, Emergency, Panic and Fire.  These should trigger your alarm so test it at a time of the day that doesn't annoy your neighbours.
- It handles F4 Image messages from Camera Sensors (and doesn't crash the integration).  This data is not processed yet but I'm working on it.
- I have added 2 Emulation Modes, "Minimal Interaction" and "Passive Monitor".  Please do not use these, they are there to support capabilities I have not yet included.
- In the settings there is a selection to include EEPROM data in the Panel Attributes. This is a minor thing but it makes it a use choice.

As said above, this is a development release. Remember that I have rewritten chunks of the code ready for future releases and as such changes have been made to the Event and Service Data. In other words your existing automations may no longer work.

The Integration update should be reversible, you should be able to delete the content of the visonic directory and put back the original version.
  • Loading branch information
davesmeghead committed Mar 22, 2024
1 parent 17d99d9 commit 315f9f9
Show file tree
Hide file tree
Showing 23 changed files with 4,264 additions and 1,997 deletions.
211 changes: 138 additions & 73 deletions custom_components/visonic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, valid_entity_id
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .pyconst import AlPanelCommand
Expand All @@ -30,20 +30,62 @@
ALARM_PANEL_RECONNECT,
ALARM_PANEL_COMMAND,
ALARM_SENSOR_BYPASS,
ALARM_SENSOR_IMAGE,
ATTR_BYPASS,
CONF_PANEL_NUMBER,
PANEL_ATTRIBUTE_NAME,
NOTIFICATION_ID,
NOTIFICATION_TITLE,
BINARY_SENSOR_STR,
IMAGE_SENSOR_STR,
SWITCH_STR,
SELECT_STR,
CONF_EMULATION_MODE,
CONF_COMMAND,
available_emulation_modes,
)

#from .create_schema import VisonicSchema

_LOGGER = logging.getLogger(__name__)

# the 5 schemas for the HA service calls
ALARM_SCHEMA_EVENTLOG = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

ALARM_SCHEMA_COMMAND = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(CONF_COMMAND) : vol.In([x.lower().replace("_"," ").title() for x in list(AlPanelCommand.get_variables().keys())]),
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

ALARM_SCHEMA_RECONNECT = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
}
)

ALARM_SCHEMA_BYPASS = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_BYPASS, default=False): cv.boolean,
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

ALARM_SCHEMA_IMAGE = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
}
)

configured_panel_list = []


def configured_hosts(hass):
"""Return a set of the configured hosts."""
return len(hass.config_entries.async_entries(DOMAIN))
Expand All @@ -68,54 +110,31 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
_LOGGER.debug("Migrating from version %s", config_entry.version)

if config_entry.version == 1:
new = {**config_entry.data}
# TODO: modify Config Entry data

new = config_entry.data.copy()
CONF_FORCE_STANDARD = "force_standard"

_LOGGER.debug(f" Migrating CONF_FORCE_STANDARD from {config_entry.data[CONF_FORCE_STANDARD]}")
if isinstance(config_entry.data[CONF_FORCE_STANDARD], bool):
_LOGGER.debug(f" Migrating CONF_FORCE_STANDARD from {config_entry.data[CONF_FORCE_STANDARD]} and its boolean")
if config_entry.data[CONF_FORCE_STANDARD]:
_LOGGER.info(f" Migration: Force standard set so using {available_emulation_modes[1]}")
new[CONF_EMULATION_MODE] = available_emulation_modes[1]
else:
_LOGGER.info(f" Migration: Force standard not set so using {available_emulation_modes[0]}")
new[CONF_EMULATION_MODE] = available_emulation_modes[0]

#del new[CONF_FORCE_STANDARD]
config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=new, options=new)
#config_entry.data = {**new}
#config_entry.version = 2

#_LOGGER.info(f"Migration to version {config_entry.version} successful")
_LOGGER.info(f" Emulation mode set to {config_entry.data[CONF_EMULATION_MODE]}")

# return False # when any changes have been made
#return False # when any changes have failed

_LOGGER.info("Migration to version %s successful", config_entry.version)
return True

CONF_PANEL = PANEL_ATTRIBUTE_NAME # this must match the field name in services.yaml
CONF_COMMAND = "command"

# the 4 schemas for the HA service calls
ALARM_SCHEMA_EVENTLOG = vol.Schema(
{
vol.Optional(CONF_PANEL, default=0): cv.positive_int,
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

ALARM_SCHEMA_COMMAND = vol.Schema(
{
vol.Required(CONF_COMMAND) : cv.enum(AlPanelCommand),
vol.Optional(CONF_PANEL, default=0): cv.positive_int,
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

ALARM_SCHEMA_RECONNECT = vol.Schema(
{
vol.Optional(CONF_PANEL, default=0): cv.positive_int,
}
)

ALARM_SCHEMA_BYPASS = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_BYPASS, default=False): cv.boolean,
vol.Optional(ATTR_CODE, default=""): cv.string,
vol.Optional(CONF_PANEL, default=0): cv.positive_int,
}
)

configured_panel_list = []

async def async_setup(hass: HomeAssistant, base_config: dict):
"""Set up the visonic component."""

Expand All @@ -129,16 +148,28 @@ def sendHANotification(message: str):
def getClient(call):
"""Lookup the panel number from the service call and find the client for that panel"""
if isinstance(call.data, dict):
if CONF_PANEL in call.data:
# This should always succeed as we set 0 as the default in the Schema
panel = call.data[CONF_PANEL]
# Check each connection to get the requested panel
for entry in hass.config_entries.async_entries(DOMAIN):
client = hass.data[DOMAIN][DOMAINCLIENT][entry.entry_id]
if client is not None:
if panel == client.getPanelID():
return client, panel
return None, panel
_LOGGER.info(f"getClient called {call.data}")
# 'entity_id': 'alarm_control_panel.visonic_alarm'
if ATTR_ENTITY_ID in call.data:
eid = str(call.data[ATTR_ENTITY_ID])
if valid_entity_id(eid):
mybpstate = hass.states.get(eid)
if mybpstate is not None:
if PANEL_ATTRIBUTE_NAME in mybpstate.attributes:
panel = mybpstate.attributes[PANEL_ATTRIBUTE_NAME]
# Check each connection to get the requested panel
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.entry_id in hass.data[DOMAIN][DOMAINCLIENT]:
client = hass.data[DOMAIN][DOMAINCLIENT][entry.entry_id]
if client is not None:
if panel == client.getPanelID():
#_LOGGER.info(f"getClient success, found client and panel")
return client, panel
else:
_LOGGER.info(f"getClient unknown entry ID {entry.entry_id}")
return None, panel
else:
_LOGGER.info(f"getClient called invalid entity ID {eid}")
return None, None

async def service_panel_eventlog(call):
Expand Down Expand Up @@ -185,32 +216,45 @@ async def service_sensor_bypass(call):
else:
sendHANotification(f"Service Panel sensor bypass failed - Panel not found")

async def handle_reload(service):
"""Handle reload service call."""
_LOGGER.info("Domain {0} Service {1} reload called: reloading integration".format(DOMAIN, service))

current_entries = hass.config_entries.async_entries(DOMAIN)

reload_tasks = [
hass.config_entries.async_reload(entry.entry_id)
for entry in current_entries
]

await asyncio.gather(*reload_tasks)
async def service_sensor_image(call):
"""Handler for sensor image service"""
_LOGGER.info("Service Panel sensor image update called")
client, panel = getClient(call)
if client is not None:
await client.service_sensor_image(call)
elif panel is not None:
sendHANotification(f"Service sensor image update - Panel {panel} not found")
else:
sendHANotification(f"Service sensor image update failed - Panel not found")

# async def handle_reload(service):
# """Handle reload service call."""
# _LOGGER.info("Domain {0} Service {1} reload called: reloading integration".format(DOMAIN, service))
#
# current_entries = hass.config_entries.async_entries(DOMAIN)
#
# reload_tasks = [
# hass.config_entries.async_reload(entry.entry_id)
# for entry in current_entries
# ]
#
# await asyncio.gather(*reload_tasks)

_LOGGER.info("Starting Visonic Component")
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DOMAINDATA] = {}
hass.data[DOMAIN][DOMAINCLIENT] = {}
hass.data[DOMAIN][DOMAINCLIENTTASK] = {}
hass.data[DOMAIN][VISONIC_UPDATE_LISTENER] = {}
# Empty out the lists

# Empty out the lists (these are no longer used in Version 2)
hass.data[DOMAIN][BINARY_SENSOR_STR] = list()
hass.data[DOMAIN][IMAGE_SENSOR_STR] = list()
hass.data[DOMAIN][SELECT_STR] = list()
hass.data[DOMAIN][SWITCH_STR] = list()
hass.data[DOMAIN][ALARM_PANEL_ENTITY] = list()

# Install the 4 handlers for the HA service calls
# Install the 5 handlers for the HA service calls
hass.services.async_register(
DOMAIN,
ALARM_PANEL_EVENTLOG,
Expand All @@ -235,11 +279,17 @@ async def handle_reload(service):
service_sensor_bypass,
schema=ALARM_SCHEMA_BYPASS,
)
hass.helpers.service.async_register_admin_service(
hass.services.async_register(
DOMAIN,
SERVICE_RELOAD,
handle_reload,
ALARM_SENSOR_IMAGE,
service_sensor_image,
schema=ALARM_SCHEMA_IMAGE,
)
# hass.helpers.service.async_register_admin_service(
# DOMAIN,
# SERVICE_RELOAD,
# handle_reload,
# )
return True

# This function is called with the flow data to create a client connection to the alarm panel
Expand All @@ -254,7 +304,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

# remove all old settings for this component, previous versions of this integration
hass.data[DOMAIN][entry.entry_id] = {}

# Empty out the lists
hass.data[DOMAIN][entry.entry_id][BINARY_SENSOR_STR] = list()
hass.data[DOMAIN][entry.entry_id][IMAGE_SENSOR_STR] = list()
hass.data[DOMAIN][entry.entry_id][SELECT_STR] = list()
hass.data[DOMAIN][entry.entry_id][SWITCH_STR] = list()
hass.data[DOMAIN][entry.entry_id][ALARM_PANEL_ENTITY] = list()

_LOGGER.info("[Visonic Setup] Starting Visonic with entry id={0} configured panels={1}".format(entry.entry_id, configured_hosts(hass)))

# combine and convert python settings map to dictionary
Expand Down Expand Up @@ -312,6 +368,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):

eid = entry.entry_id

hass.services.async_remove(DOMAIN, ALARM_PANEL_EVENTLOG)
hass.services.async_remove(DOMAIN, ALARM_PANEL_RECONNECT)
hass.services.async_remove(DOMAIN, ALARM_PANEL_COMMAND)
hass.services.async_remove(DOMAIN, ALARM_SENSOR_BYPASS)
hass.services.async_remove(DOMAIN, ALARM_SENSOR_IMAGE)

client = hass.data[DOMAIN][DOMAINCLIENT][eid]
clientTask = hass.data[DOMAIN][DOMAINCLIENTTASK][eid]
updateListener = hass.data[DOMAIN][VISONIC_UPDATE_LISTENER][eid]
Expand All @@ -332,6 +394,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
del hass.data[DOMAIN][DOMAINCLIENTTASK][eid]
del hass.data[DOMAIN][VISONIC_UPDATE_LISTENER][eid]

#if hass.data[DOMAIN][eid]:
# hass.data[DOMAIN].pop(eid)
#
if panelid in configured_panel_list:
configured_panel_list.remove(panelid)
else:
Expand Down
26 changes: 19 additions & 7 deletions custom_components/visonic/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from enum import IntEnum

from homeassistant.auth.permissions.const import POLICY_CONTROL
from .pyconst import AlPanelCommand, AlPanelStatus
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel.const import (
AlarmControlPanelEntityFeature,
Expand All @@ -34,6 +33,7 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .client import VisonicClient
from .pyconst import AlPanelCommand, AlPanelStatus
from .const import (
DOMAIN,
DOMAINCLIENT,
Expand All @@ -55,11 +55,12 @@ async def async_setup_entry(
if DOMAIN in hass.data:
# Get the client
client = hass.data[DOMAIN][DOMAINCLIENT][entry.entry_id]
# Create the alarm controlpanel
va = VisonicAlarm(client, 1)
# Add it to HA
devices = [va]
async_add_entities(devices, True)
if not client.isDisableAllCommands():
# Create the alarm controlpanel (partition id = 1)
va = VisonicAlarm(client, 1)
# Add it to HA
devices = [va]
async_add_entities(devices, True)

platform = entity_platform.async_get_current_platform()
_LOGGER.debug(f"alarm control panel async_setup_entry called {platform}")
Expand Down Expand Up @@ -202,6 +203,8 @@ def extra_state_attributes(self): #
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
if self._client.isDisableAllCommands():
return 0
#_LOGGER.debug(f"[AlarmcontrolPanel] Getting Supported Features {self._client.isArmHome()} {self._client.isArmNight()}")
retval = AlarmControlPanelEntityFeature.ARM_AWAY
if self._client.isArmNight():
Expand All @@ -210,6 +213,9 @@ def supported_features(self) -> int:
if self._client.isArmHome():
#_LOGGER.debug("[AlarmcontrolPanel] Adding Home")
retval = retval | AlarmControlPanelEntityFeature.ARM_HOME
if self._client.isPowerMaster():
#_LOGGER.debug("[AlarmcontrolPanel] Adding Trigger")
retval = retval | AlarmControlPanelEntityFeature.TRIGGER
return retval

# DO NOT OVERRIDE state_attributes AS IT IS USED IN THE LOVELACE FRONTEND TO DETERMINE code_format
Expand All @@ -219,6 +225,8 @@ def code_format(self):
"""Regex for code format or None if no code is required."""
# Do not show the code panel if the integration is just starting up and
# connecting to the panel
if self._client.isDisableAllCommands():
return None
if self.isPanelConnected():
return CodeFormat.NUMBER if self._client.isCodeRequired() else None
return None
Expand Down Expand Up @@ -253,7 +261,11 @@ def alarm_arm_away(self, code=None):

def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
raise NotImplementedError()
if not self.isPanelConnected():
raise HomeAssistantError(f"Visonic Integration {self._myname} not connected to panel.")
if self._client.isPowerMaster():
self._client.sendCommand("Trigger Siren", AlPanelCommand.TRIGGER , code)
#self._client.sendCommand("Arm Away", command, code)

def alarm_arm_custom_bypass(self, data=None):
"""Bypass Panel."""
Expand Down
Loading

0 comments on commit 315f9f9

Please sign in to comment.