Skip to content

Commit

Permalink
0.9.9.1 Significant Changes for PowerMaster and better control of pan…
Browse files Browse the repository at this point in the history
…el interaction

Lots of changes, too numerous to mention with many many bug fixes.
Added a Sequencer to centralise the low level startup and ongoing control of panel interaction
Better Standard Mode operation, more consistent
X10 updates for better service in Home Assistant, including Dimmer/Brighter
Language Translation support, English and French are 99% complete, a few others are work in progress
Restructured and Reduced Configuration settings
Remapped Visonic State to Home Assistant Alarm States
Updated Alarm Panel Entity Attributes
Home Assistant Events are more consistent, see the wiki
Entities are linked to Devices in Home Assistant
PowerMaster: Better decoding of B0 messages for PIR sensor changes (although still not 100% I think)
PowerMaster: Decoding of B0 messages in a standardised way

Work in progress:
Added a bridge interface for the future
Added an Image Entity for Camera PIR sensors for the future
Initial attempt to delete stale entities and devices, work in progress
Additional language translations
  • Loading branch information
davesmeghead committed Sep 29, 2024
1 parent 8605cc3 commit d365de4
Show file tree
Hide file tree
Showing 23 changed files with 7,258 additions and 3,334 deletions.
173 changes: 165 additions & 8 deletions custom_components/visonic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import asyncio
import requests.exceptions
import voluptuous as vol
import collections
from collections import namedtuple

from dataclasses import dataclass

Expand All @@ -16,11 +18,14 @@
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.components import persistent_notification
from homeassistant.util.hass_dict import HassKey, HassEntryKey
from homeassistant.helpers.translation import async_translate_state

from homeassistant.const import (
Platform,
ATTR_CODE,
ATTR_ENTITY_ID,
EVENT_CORE_CONFIG_UPDATE,
# EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
)

Expand Down Expand Up @@ -66,15 +71,15 @@
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.Required(CONF_COMMAND) : vol.In([x.lower() for x in list(AlPanelCommand.get_variables().keys())]),
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)

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())]),
vol.Required(CONF_X10_COMMAND) : vol.In([x.lower() for x in list(AlX10Command.get_variables().keys())]),
}
)

Expand All @@ -87,7 +92,7 @@
ALARM_SCHEMA_BYPASS = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_BYPASS, default=False): cv.boolean,
vol.Required(ATTR_BYPASS, default=False): cv.boolean,
vol.Optional(ATTR_CODE, default=""): cv.string,
}
)
Expand All @@ -99,6 +104,96 @@
)

update_version_panel_number = 0
translatedLanguageAlready = False

##############################################################################################################################################################################################################################################
########################## Known Data Strings for EEPROM and Message Decode ################################################################################################################################################################
##############################################################################################################################################################################################################################################

# Use English as the default values unless updated by the settings from the Integration
pmLogEvent_t = [
"None",
# 1
"Interior Alarm", "Perimeter Alarm", "Delay Alarm", "24h Silent Alarm", "24h Audible Alarm",
"Tamper", "Control Panel Tamper", "Tamper Alarm", "Tamper Alarm", "Communication Loss",
# 11
"Panic From Keyfob", "Panic From Control Panel", "Duress", "Confirm Alarm", "General Trouble",
"General Trouble Restore", "Interior Restore", "Perimeter Restore", "Delay Restore", "24h Silent Restore",
# 21
"24h Audible Restore", "Tamper Restore", "Control Panel Tamper Restore", "Tamper Restore", "Tamper Restore",
"Communication Restore", "General Restore", "Cancel Alarm", "Trouble Restore", "Not used",
# 31
"Recent Close", "Fire", "Fire Restore", "Not Active", "Emergency",
"Remove User", "Disarm Latchkey", "Confirm Alarm Emergency", "Supervision (Inactive)", "Supervision Restore (Active)",
# 41
"Low Battery", "Low Battery Restore", "AC Fail", "AC Restore", "Control Panel Low Battery",
"Control Panel Low Battery Restore", "RF Jamming", "RF Jamming Restore", "Communications Failure", "Communications Restore",
# 51
"Telephone Line Failure", "Telephone Line Restore", "Auto Test", "Fuse Failure", "Fuse Restore",
"Keyfob Low Battery", "Keyfob Low Battery Restore", "Engineer Reset", "Battery Disconnect", "1-Way Keypad Low Battery",
# 61
"1-Way Keypad Low Battery Restore", "1-Way Keypad Inactive", "1-Way Keypad Restore Active", "Low Battery Ack", "Clean Me",
"Fire Trouble", "Low Battery", "Battery Restore", "AC Fail", "AC Restore",
# 71
"Supervision (Inactive)", "Supervision Restore (Active)", "Gas Alert", "Gas Alert Restore", "Gas Trouble",
"Gas Trouble Restore", "Flood Alert", "Flood Alert Restore", "X-10 Trouble", "X-10 Trouble Restore",
# 81
"Armed Home", "Armed Away", "Quick Armed Home", "Quick Armed Away", "Disarmed",
"Fail To Auto-Arm", "Enter To Test Mode", "Exit From Test Mode", "Force Arm", "Auto Arm",
# 91
"Instant Arm", "Bypass", "Fail To Arm", "Door Open", "Communication Established By Control Panel",
"System Reset", "Installer Programming", "Wrong Password", "Not Sys Event", "Not Sys Event",
# 101
"Extreme Hot Alert", "Extreme Hot Alert Restore", "Freeze Alert", "Freeze Alert Restore", "Human Cold Alert",
"Human Cold Alert Restore", "Human Hot Alert", "Human Hot Alert Restore", "Temperature Sensor Trouble", "Temperature Sensor Trouble Restore",
# 111
# New values for PowerMaster and models with partitions
"PIR Mask", "PIR Mask Restore", "Repeater low battery", "Repeater low battery restore", "Repeater inactive",
"Repeater inactive restore", "Repeater tamper", "Repeater tamper restore", "Siren test end", "Devices test end",
# 121
"One way comm. trouble", "One way comm. trouble restore", "Sensor outdoor alarm", "Sensor outdoor restore", "Guard sensor alarmed",
"Guard sensor alarmed restore", "Date time change", "System shutdown", "System power up", "Missed Reminder",
# 131
"Pendant test fail", "Basic KP inactive", "Basic KP inactive restore", "Basic KP tamper", "Basic KP tamper Restore",
"Heat", "Heat restore", "LE Heat Trouble", "CO alarm", "CO alarm restore",
# 141
"CO trouble", "CO trouble restore", "Exit Installer", "Enter Installer", "Self test trouble",
"Self test restore", "Confirm panic event", "", "Soak test fail", "Fire Soak test fail",
# 151
"Gas Soak test fail"
]

# Default "Panel" to English
en_keys = ["system", "zone", "fob", "user", "pad", "siren", "2pad", "x10", "pgm", "gsm", "powerlink", "ptag", "repeater", "undefined"]

# pmax is powermax, pmas is powermaster
pmLogPowerColl = collections.namedtuple("pmLogPowerColl", 'key name pmax_include pmax_autonumber pmax_start pmax_stop pmas_include pmas_autonumber pmas_start pmas_stop' )
pmLogPower = [ # PowerMax Settings PowerMaster Settings powermax powermaster
pmLogPowerColl( en_keys[0] , "System" , True, False, 0, 0 , True, False, 0, 0 ), # 0 0 System
pmLogPowerColl( en_keys[1] , "Zone" , True, True, 1, 30 , True, True, 1, 64 ), # 1 1 Zone
pmLogPowerColl( en_keys[2] , "Fob" , True, True, 1, 8 , True, True, 1, 32 ), # 31 65 Fob
pmLogPowerColl( en_keys[3] , "User" , True, True, 1, 8 , True, True, 1, 48 ), # 39 97 User
pmLogPowerColl( en_keys[4] , "Pad" , True, True, 1, 8 , True, True, 1, 32 ), # 47 145 Pad
pmLogPowerColl( en_keys[5] , "Sir" , True, True, 1, 2 , True, True, 1, 8 ), # 55 177 Sir
pmLogPowerColl( en_keys[6] , "2Pad" , True, True, 1, 4 , True, True, 1, 4 ), # 57 185 2PAD
pmLogPowerColl( en_keys[7] , "X10" , True, True, 1, 15 , True, True, 1, 15 ), # 61 189 X10
pmLogPowerColl( en_keys[8] , "PGM" , True, False, 0, 0 , True, False, 0, 0 ), # 76 204 PGM
pmLogPowerColl( en_keys[9] , "GSM" , True, False, 0, 0 , False, False, 0, 0 ), # 77 - GSM
pmLogPowerColl( en_keys[10] , "P-LINK" , True, False, 0, 0 , True, False, 0, 0 ), # 78 205 P-LINK
pmLogPowerColl( en_keys[11] , "PTag" , True, True, 1, 8 , True, True, 1, 32 ), # 79 206 PTag
pmLogPowerColl( en_keys[12] , "Rptr" , False, False, 0, 0 , True, True, 1, 8 ), # - 238 Rptr
pmLogPowerColl( en_keys[13] , "Unknown", True, False, 1, 41 , True, False, 1, 10 ) # 87 246 Unknown
]

# Create the defaults in English to be updated by settings from the Integration
pmLogPowerMaxUser_t = []
pmLogPowerMasterUser_t = []
for v in pmLogPower:
# create list
if v.pmax_include:
pmLogPowerMaxUser_t.extend([f"{v.name} {i:>02}" if v.pmax_autonumber else v.name for i in range(v.pmax_start, v.pmax_stop+1)])
if v.pmas_include:
pmLogPowerMasterUser_t.extend([f"{v.name} {i:>02}" if v.pmas_autonumber else v.name for i in range(v.pmas_start, v.pmas_stop+1)])

# Create the types for the Configuration Parameter Entry
VisonicConfigKey: HassEntryKey["VisonicConfigData"] = HassEntryKey(DOMAIN)
Expand All @@ -110,7 +205,7 @@ class VisonicConfigData:
sensors: list()
# Made it a class just in case I want to include more parameters in future

def findClient(hass, panel : int):
def findClient(hass, panel : int) -> VisonicClient | None:
"""Look through all the configuration entries looking for the panel."""
data = hass.data[VisonicConfigKey]
for entry_id in data:
Expand All @@ -134,6 +229,56 @@ async def combineSettings(entry):
conf[k] = entry.options[k]
return conf

def translateLanguage(hass):
###################################################################################################
# Retrieve the names of the things that create the events from the language translations files ####
###################################################################################################

_LOGGER.debug(f"[translateLanguage] Home Assistant country is {str(hass.config.country)}")
_LOGGER.debug(f"[translateLanguage] Home Assistant language is {str(hass.config.language)}")

en_vals = { key : async_translate_state(hass=hass,
domain=DOMAIN,
device_class="event_name",
state=key,
platform=None,
translation_key=None)
for key in en_keys }

_LOGGER.debug(f"[translateLanguage] alarm control panel event_names translations {en_vals}")

if len(en_vals) > 0:
pmLogPowerMaxUser_t = []
pmLogPowerMasterUser_t = []
for v in pmLogPower:
# Use the translation if in the list else default back to the English.
# The translation file does not need to contain all 14 translations
w = en_vals[v.key] if v.key in en_vals else v.name # get the translation
# create list
if v.pmax_include:
pmLogPowerMaxUser_t.extend([f"{w} {i:>02}" if v.pmax_autonumber else w for i in range(v.pmax_start, v.pmax_stop+1)])
if v.pmas_include:
pmLogPowerMasterUser_t.extend([f"{w} {i:>02}" if v.pmas_autonumber else w for i in range(v.pmas_start, v.pmas_stop+1)])

_LOGGER.debug(f"[translateLanguage] Replacing default English names with provided list:")
_LOGGER.debug(f"[translateLanguage] pmLogPowerMaxUser_t = {pmLogPowerMaxUser_t}")
_LOGGER.debug(f"[translateLanguage] pmLogPowerMasterUser_t = {pmLogPowerMasterUser_t}")

# Retrieve the actions from the language translations files
for key in range(0, len(pmLogEvent_t)+1):
state = f"{key:0>3}"
tx_s = async_translate_state(hass=hass,
domain=DOMAIN,
device_class="event_action",
state=state,
platform=None,
translation_key=None)
if tx_s != state: # Check to see if it's just returned the state that I passed in i.e. to make sure the translation exists
pmLogEvent_t[key] = tx_s

_LOGGER.debug(f"[translateLanguage] alarm control panel event_action translated {pmLogEvent_t}")


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

Expand Down Expand Up @@ -274,8 +419,6 @@ async def handle_reload(call) -> None:
service_sensor_bypass,
schema=ALARM_SCHEMA_BYPASS,
)


# hass.services.async_register(
# DOMAIN,
# ALARM_SENSOR_IMAGE,
Expand All @@ -295,6 +438,7 @@ async def handle_reload(call) -> None:
# - the original control flow if it existed
async def async_setup_entry(hass: HomeAssistant, entry: VisonicConfigEntry) -> bool:
"""Set up visonic from a config entry."""
global translatedLanguageAlready

def configured_hosts(hass):
"""Return a set of the configured hosts."""
Expand All @@ -304,6 +448,19 @@ def configured_hosts(hass):
#_LOGGER.debug(f"[Visonic Setup] Entry data={entry.data} options={entry.options}")
_LOGGER.debug(f"[Visonic Setup] Entry id={entry.entry_id} in a total of {configured_hosts(hass)} previously configured panels")

# Listener to handle fired events
def handle_core_config_updated(event):
_LOGGER.debug(f"[Visonic Setup] event {str(event)}")
#hass = async_get_hass()
translateLanguage(hass)

if not translatedLanguageAlready:
translatedLanguageAlready = True
translateLanguage(hass)
# Listen for when EVENT_CORE_CONFIG_UPDATE is fired
# hass.bus.async_listen(EVENT_HOMEASSISTANT_STARTED, handle_core_config_updated)
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, handle_core_config_updated)

# combine and convert python settings map to dictionary
conf = await combineSettings(entry)

Expand All @@ -317,7 +474,7 @@ def configured_hosts(hass):

# Check for unique panel ids or HA gets really confused and we end up make a big mess in the config files.
if findClient(hass, panel_id) is not None:
_LOGGER.warning(f"[Visonic Setup] The Panel Number {panel_id} is not Unique, you already have a Panel with this Number")
_LOGGER.error(f"[Visonic Setup] The Panel Number {panel_id} is not Unique, you already have a Panel with this Number")
return False

# When here, panel_id should be unique in the panels configured so far.
Expand Down Expand Up @@ -435,7 +592,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: VisonicConfigEntry):
if data.client is not None:
p = data.client.getPanelID()
# stop all activity in the client
unload_ok = await data.client.service_panel_stop()
unload_ok = await data.client.async_service_panel_stop()

if entry.entry_id in hass.data[VisonicConfigKey]:
hass.data[VisonicConfigKey].pop(entry.entry_id)
Expand Down
Loading

0 comments on commit d365de4

Please sign in to comment.