2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.paradoxalarm.internal.communication;
15 import java.io.IOException;
16 import java.net.UnknownHostException;
17 import java.nio.charset.StandardCharsets;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
26 import org.openhab.binding.paradoxalarm.internal.communication.messages.EpromRequestPayload;
27 import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
28 import org.openhab.binding.paradoxalarm.internal.communication.messages.IPayload;
29 import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
30 import org.openhab.binding.paradoxalarm.internal.communication.messages.RamRequestPayload;
31 import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
32 import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
33 import org.openhab.binding.paradoxalarm.internal.model.EntityType;
34 import org.openhab.binding.paradoxalarm.internal.model.PanelType;
35 import org.openhab.binding.paradoxalarm.internal.model.ZoneStateFlags;
36 import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * The {@link EvoCommunicator} is responsible for handling communication to Evo192 alarm system via IP150 interface.
43 * @author Konstantin Polihronov - Initial contribution
45 public class EvoCommunicator extends GenericCommunicator implements IParadoxCommunicator {
47 private static final byte RAM_BLOCK_SIZE = (byte) 64;
49 private final Logger logger = LoggerFactory.getLogger(EvoCommunicator.class);
51 private MemoryMap memoryMap;
53 private Map<EntityType, Map<Integer, String>> entityLabelsMap = new HashMap<>();
55 private PanelType panelType = PanelType.UNKNOWN;
56 private Integer maxPartitions;
57 private Integer maxZones;
59 private EvoCommunicator(String ipAddress, int tcpPort, String ip150Password, String pcPassword,
60 ScheduledExecutorService scheduler, PanelType panelType, Integer maxPartitions, Integer maxZones,
61 boolean useEncryption) throws UnknownHostException, IOException {
62 super(ipAddress, tcpPort, ip150Password, pcPassword, scheduler, useEncryption);
63 this.panelType = panelType;
64 this.maxPartitions = maxPartitions;
65 this.maxZones = maxZones;
66 logger.debug("PanelType={}, Max Partitions={}, Max Zones={}", panelType, maxPartitions, maxZones);
67 initializeMemoryMap();
71 protected void receiveEpromResponse(IResponse response) {
72 byte[] payload = response.getPayload();
73 if (payload != null) {
74 EpromRequest request = (EpromRequest) response.getRequest();
75 int entityId = request.getEntityId();
76 EntityType entityType = request.getEntityType();
77 updateEntityLabel(entityType, entityId, payload);
79 logger.debug("Wrong parsed result. Probably wrong data received in response. Response={}", response);
85 protected void receiveRamResponse(IResponse response) {
86 byte[] payload = response.getPayload();
87 if (payload != null && payload.length >= RAM_BLOCK_SIZE) {
88 RamRequest request = (RamRequest) response.getRequest();
89 int ramBlockNumber = request.getRamBlockNumber();
90 memoryMap.updateElement(ramBlockNumber, payload);
91 if (logger.isTraceEnabled()) {
92 logger.trace("Result for ramBlock={} is [{}]", ramBlockNumber, ParadoxUtil.byteArrayToString(payload));
95 // Trigger listeners update when last memory page update is received
96 if (ramBlockNumber == panelType.getRamPagesNumber()) {
100 logger.debug("Wrong parsed result. Probably wrong data received in response");
105 private void updateEntityLabel(EntityType entityType, int entityId, byte[] payload) {
106 String label = createString(payload);
107 logger.debug("{} label updated to: {}", entityType, label);
109 entityLabelsMap.get(entityType).put(entityId, label);
112 private void retrievePartitionLabel(int partitionNo) {
113 logger.debug("Submitting request for partition label: {}", partitionNo);
114 int address = 0x3A6B + (partitionNo) * 107;
115 byte labelLength = 16;
118 IPayload payload = new EpromRequestPayload(address, labelLength);
119 ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
121 IRequest epromRequest = new EpromRequest(partitionNo, EntityType.PARTITION, readEpromIPPacket, this);
122 submitRequest(epromRequest);
123 } catch (ParadoxException e) {
124 logger.debug("Error creating request for with number={}, Exception={} ", partitionNo, e.getMessage());
128 private ParadoxIPPacket createSerialPassthroughPacket(IPayload payload) {
129 ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes());
130 packet.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST).setUnknown0((byte) 0x14);
134 private void retrieveZoneLabel(int zoneNumber) {
135 logger.debug("Submitting request for zone label: {}", zoneNumber);
136 final byte labelLength = 16;
140 if (zoneNumber < 96) {
141 address = 0x430 + (zoneNumber) * 16;
143 address = 0x62F7 + (zoneNumber - 96) * 16;
146 IPayload payload = new EpromRequestPayload(address, labelLength);
147 ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
149 IRequest epromRequest = new EpromRequest(zoneNumber, EntityType.ZONE, readEpromIPPacket, this);
150 submitRequest(epromRequest);
151 } catch (ParadoxException e) {
152 logger.debug("Error creating request with number={}, Exception={} ", zoneNumber, e.getMessage());
157 public List<byte[]> getPartitionFlags() {
158 List<byte[]> result = new ArrayList<>();
160 byte[] element = memoryMap.getElement(2);
161 byte[] firstBlock = Arrays.copyOfRange(element, 32, 64);
163 element = memoryMap.getElement(3);
164 byte[] secondBlock = Arrays.copyOfRange(element, 0, 16);
165 byte[] mergeByteArrays = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
166 for (int i = 0; i < mergeByteArrays.length; i += 6) {
167 result.add(Arrays.copyOfRange(mergeByteArrays, i, i + 6));
174 public ZoneStateFlags getZoneStateFlags() {
175 ZoneStateFlags result = new ZoneStateFlags();
177 byte[] firstPage = memoryMap.getElement(0);
178 byte[] secondPage = memoryMap.getElement(8);
180 int pageOffset = panelType == PanelType.EVO48 ? 34 : 40;
181 byte[] firstBlock = Arrays.copyOfRange(firstPage, 28, pageOffset);
182 if (panelType != PanelType.EVO192) {
183 result.setZonesOpened(firstBlock);
185 byte[] secondBlock = Arrays.copyOfRange(secondPage, 0, 12);
186 byte[] zonesOpened = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
187 result.setZonesOpened(zonesOpened);
190 pageOffset = panelType == PanelType.EVO48 ? 46 : 52;
191 firstBlock = Arrays.copyOfRange(firstPage, 40, pageOffset);
192 if (panelType != PanelType.EVO192) {
193 result.setZonesTampered(firstBlock);
195 byte[] secondBlock = Arrays.copyOfRange(secondPage, 12, 24);
196 byte[] zonesTampered = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
197 result.setZonesTampered(zonesTampered);
200 pageOffset = panelType == PanelType.EVO48 ? 58 : 64;
201 firstBlock = Arrays.copyOfRange(firstPage, 52, pageOffset);
202 if (panelType != PanelType.EVO192) {
203 result.setZonesTampered(firstBlock);
205 byte[] secondBlock = Arrays.copyOfRange(secondPage, 24, 36);
206 byte[] zonesLowBattery = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
207 result.setZonesLowBattery(zonesLowBattery);
213 public void initializeMemoryMap() {
214 for (EntityType type : EntityType.values()) {
215 entityLabelsMap.put(type, new HashMap<>());
218 List<byte[]> ramCache = new ArrayList<>(panelType.getRamPagesNumber() + 1);
219 for (int i = 0; i <= panelType.getRamPagesNumber(); i++) {
220 ramCache.add(new byte[0]);
222 memoryMap = new MemoryMap(ramCache);
226 public void refreshMemoryMap() {
228 logger.debug("Attempt to refresh memory map was made, but communicator is not online. Skipping update.");
232 SyncQueue queue = SyncQueue.getInstance();
233 synchronized (queue) {
234 for (int i = 1; i <= panelType.getRamPagesNumber(); i++) {
240 private void submitRamRequest(int blockNo) {
242 logger.trace("Creating RAM page {} read request", blockNo);
243 IPayload payload = new RamRequestPayload(blockNo, RAM_BLOCK_SIZE);
244 ParadoxIPPacket ipPacket = createSerialPassthroughPacket(payload);
245 IRequest ramRequest = new RamRequest(blockNo, ipPacket, this);
246 submitRequest(ramRequest);
247 } catch (ParadoxException e) {
249 "Unable to create request payload from provided bytes to read. blockNo={}, bytes to read={}. Exception={}",
250 blockNo, RAM_BLOCK_SIZE, e.getMessage());
254 private String createString(byte[] payloadResult) {
255 return new String(payloadResult, StandardCharsets.US_ASCII);
259 public void executeCommand(String command) {
260 IP150Command ip150Command = IP150Command.valueOf(command);
261 switch (ip150Command) {
263 startLoginSequence();
266 CommunicationState.LOGOUT.runPhase(this);
269 CommunicationState.LOGOUT.runPhase(this);
270 scheduler.schedule(this::startLoginSequence, 5, TimeUnit.SECONDS);
273 logger.debug("Command {} not implemented.", command);
278 public MemoryMap getMemoryMap() {
282 public Map<EntityType, Map<Integer, String>> getEntityLabelsMap() {
283 return entityLabelsMap;
287 public Map<Integer, String> getPartitionLabels() {
288 return entityLabelsMap.get(EntityType.PARTITION);
292 public Map<Integer, String> getZoneLabels() {
293 return entityLabelsMap.get(EntityType.ZONE);
297 public void initializeData() {
298 synchronized (SyncQueue.getInstance()) {
299 initializeEpromData();
304 private void initializeEpromData() {
305 for (int i = 0; i < maxPartitions; i++) {
306 retrievePartitionLabel(i);
308 for (int i = 0; i < maxZones; i++) {
309 retrieveZoneLabel(i);
313 public static class EvoCommunicatorBuilder implements ICommunicatorBuilder {
315 private final Logger logger = LoggerFactory.getLogger(EvoCommunicatorBuilder.class);
317 // Mandatory parameters
318 private PanelType panelType;
319 private String ipAddress;
320 private String ip150Password;
321 private ScheduledExecutorService scheduler;
323 // Non mandatory or with predefined values
324 private Integer maxPartitions;
325 private Integer maxZones;
326 private int tcpPort = 10000;
327 private String pcPassword = "0000";
329 private boolean useEncryption;
331 EvoCommunicatorBuilder(PanelType panelType) {
332 this.panelType = panelType;
336 public IParadoxCommunicator build() {
337 if (ipAddress == null || ipAddress.isEmpty()) {
338 final String msg = "IP address cannot be empty !";
340 throw new ParadoxRuntimeException(msg);
343 if (ip150Password == null || ip150Password.isEmpty()) {
344 final String msg = "Password for IP150 cannot be empty !";
346 throw new ParadoxRuntimeException(msg);
349 if (scheduler == null) {
350 final String msg = "Scheduler is mandatory parameter !";
352 throw new ParadoxRuntimeException(msg);
355 if (maxPartitions == null || maxPartitions < 1) {
356 this.maxPartitions = panelType.getPartitions();
359 if (maxZones == null || maxZones < 1) {
360 this.maxZones = panelType.getZones();
364 return new EvoCommunicator(ipAddress, tcpPort, ip150Password, pcPassword, scheduler, panelType,
365 maxPartitions, maxZones, useEncryption);
366 } catch (IOException e) {
367 logger.warn("Unable to create communicator for Panel={}. Message={}", panelType, e.getMessage());
368 throw new ParadoxRuntimeException(e);
373 public ICommunicatorBuilder withMaxZones(Integer maxZones) {
374 this.maxZones = maxZones;
379 public ICommunicatorBuilder withMaxPartitions(Integer maxPartitions) {
380 this.maxPartitions = maxPartitions;
385 public ICommunicatorBuilder withIp150Password(String ip150Password) {
386 this.ip150Password = ip150Password;
391 public ICommunicatorBuilder withPcPassword(String pcPassword) {
392 this.pcPassword = pcPassword;
397 public ICommunicatorBuilder withIpAddress(String ipAddress) {
398 this.ipAddress = ipAddress;
403 public ICommunicatorBuilder withTcpPort(Integer tcpPort) {
404 this.tcpPort = tcpPort;
409 public ICommunicatorBuilder withScheduler(ScheduledExecutorService scheduler) {
410 this.scheduler = scheduler;
415 public ICommunicatorBuilder withEncryption(boolean useEncryption) {
416 this.useEncryption = useEncryption;