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 - 1, 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);
179 createZoneOpenedFlags(result, firstPage, secondPage);
180 createZoneTamperedFlags(result, firstPage, secondPage);
181 createZoneLowbatteryFlags(result, firstPage, secondPage);
183 createSpecialZoneFlags(result, memoryMap);
185 ParadoxUtil.printByteArray("Zone opened flags", result.getZonesOpened());
186 ParadoxUtil.printByteArray("Zone tampered flags", result.getZonesTampered());
187 ParadoxUtil.printByteArray("Zone low battery flags", result.getZonesLowBattery());
188 ParadoxUtil.printByteArray("Zone special flags", result.getZoneSpecialFlags());
193 private void createZoneOpenedFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
194 int pageOffset = panelType == PanelType.EVO48 ? 34 : 40;
195 byte[] firstBlock = Arrays.copyOfRange(firstPage, 28, pageOffset);
196 if (panelType != PanelType.EVO192) {
197 result.setZonesOpened(firstBlock);
199 byte[] secondBlock = Arrays.copyOfRange(secondPage, 0, 12);
200 byte[] zonesOpened = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
201 result.setZonesOpened(zonesOpened);
205 private void createZoneTamperedFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
206 int pageOffset = panelType == PanelType.EVO48 ? 46 : 52;
207 byte[] firstBlock = Arrays.copyOfRange(firstPage, 40, pageOffset);
208 if (panelType != PanelType.EVO192) {
209 result.setZonesTampered(firstBlock);
211 byte[] secondBlock = Arrays.copyOfRange(secondPage, 12, 24);
212 byte[] zonesTampered = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
213 result.setZonesTampered(zonesTampered);
217 private void createZoneLowbatteryFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
218 int pageOffset = panelType == PanelType.EVO48 ? 58 : 64;
219 byte[] firstBlock = Arrays.copyOfRange(firstPage, 52, pageOffset);
220 if (panelType != PanelType.EVO192) {
221 result.setZonesLowBattery(firstBlock);
223 byte[] secondBlock = Arrays.copyOfRange(secondPage, 24, 36);
224 byte[] zonesLowBattery = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
225 result.setZonesLowBattery(zonesLowBattery);
229 @SuppressWarnings("incomplete-switch")
230 private void createSpecialZoneFlags(ZoneStateFlags result, MemoryMap memoryMap) {
231 byte[] page2 = memoryMap.getElement(1);
232 byte[] page3 = memoryMap.getElement(2);
233 byte[] page7 = memoryMap.getElement(8);
234 byte[] page8 = memoryMap.getElement(9);
235 byte[] page9 = memoryMap.getElement(10);
239 byte[] firstBlock = Arrays.copyOfRange(page2, 0, 48);
240 result.setZoneSpecialFlags(firstBlock);
243 firstBlock = Arrays.copyOf(page2, 64);
244 byte[] secondBlock = Arrays.copyOfRange(page3, 0, 32);
245 byte[] specialZoneFlags = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
246 result.setZoneSpecialFlags(specialZoneFlags);
250 firstBlock = Arrays.copyOf(page2, 64);
251 secondBlock = Arrays.copyOfRange(page3, 0, 32);
252 byte[] thirdBlock = Arrays.copyOfRange(page7, 36, 64);
253 byte[] fourthBlock = Arrays.copyOf(page8, 64);
254 byte[] fifthBlock = Arrays.copyOfRange(page9, 0, 4);
255 specialZoneFlags = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock, thirdBlock, fourthBlock,
257 result.setZoneSpecialFlags(specialZoneFlags);
262 public void initializeMemoryMap() {
263 for (EntityType type : EntityType.values()) {
264 entityLabelsMap.put(type, new HashMap<>());
267 List<byte[]> ramCache = new ArrayList<>(panelType.getRamPagesNumber() + 1);
268 for (int i = 0; i <= panelType.getRamPagesNumber(); i++) {
269 ramCache.add(new byte[0]);
271 memoryMap = new MemoryMap(ramCache);
275 public void refreshMemoryMap() {
277 logger.debug("Attempt to refresh memory map was made, but communicator is not online. Skipping update.");
281 SyncQueue queue = SyncQueue.getInstance();
282 synchronized (queue) {
283 for (int i = 1; i <= panelType.getRamPagesNumber(); i++) {
289 private void submitRamRequest(int blockNo) {
291 logger.trace("Creating RAM page {} read request", blockNo);
292 IPayload payload = new RamRequestPayload(blockNo, RAM_BLOCK_SIZE);
293 ParadoxIPPacket ipPacket = createSerialPassthroughPacket(payload);
294 IRequest ramRequest = new RamRequest(blockNo, ipPacket, this);
295 submitRequest(ramRequest);
296 } catch (ParadoxException e) {
298 "Unable to create request payload from provided bytes to read. blockNo={}, bytes to read={}. Exception={}",
299 blockNo, RAM_BLOCK_SIZE, e.getMessage());
303 private String createString(byte[] payloadResult) {
304 return new String(payloadResult, StandardCharsets.US_ASCII);
308 public void executeCommand(String command) {
309 IP150Command ip150Command = IP150Command.valueOf(command);
310 switch (ip150Command) {
312 startLoginSequence();
315 CommunicationState.LOGOUT.runPhase(this);
318 CommunicationState.LOGOUT.runPhase(this);
319 scheduler.schedule(this::startLoginSequence, 5, TimeUnit.SECONDS);
322 logger.debug("Command {} not implemented.", command);
327 public MemoryMap getMemoryMap() {
331 public Map<EntityType, Map<Integer, String>> getEntityLabelsMap() {
332 return entityLabelsMap;
336 public Map<Integer, String> getPartitionLabels() {
337 return entityLabelsMap.get(EntityType.PARTITION);
341 public Map<Integer, String> getZoneLabels() {
342 return entityLabelsMap.get(EntityType.ZONE);
346 public void initializeData() {
347 synchronized (SyncQueue.getInstance()) {
348 initializeEpromData();
353 private void initializeEpromData() {
354 for (int i = 0; i < maxPartitions; i++) {
355 retrievePartitionLabel(i);
357 for (int i = 0; i < maxZones; i++) {
358 retrieveZoneLabel(i);
362 public static class EvoCommunicatorBuilder implements ICommunicatorBuilder {
364 private final Logger logger = LoggerFactory.getLogger(EvoCommunicatorBuilder.class);
366 // Mandatory parameters
367 private PanelType panelType;
368 private String ipAddress;
369 private String ip150Password;
370 private ScheduledExecutorService scheduler;
372 // Non mandatory or with predefined values
373 private Integer maxPartitions;
374 private Integer maxZones;
375 private int tcpPort = 10000;
376 private String pcPassword = "0000";
378 private boolean useEncryption;
380 EvoCommunicatorBuilder(PanelType panelType) {
381 this.panelType = panelType;
385 public IParadoxCommunicator build() {
386 if (ipAddress == null || ipAddress.isEmpty()) {
387 final String msg = "IP address cannot be empty !";
389 throw new ParadoxRuntimeException(msg);
392 if (ip150Password == null || ip150Password.isEmpty()) {
393 final String msg = "Password for IP150 cannot be empty !";
395 throw new ParadoxRuntimeException(msg);
398 if (scheduler == null) {
399 final String msg = "Scheduler is mandatory parameter !";
401 throw new ParadoxRuntimeException(msg);
404 if (maxPartitions == null || maxPartitions < 1) {
405 this.maxPartitions = panelType.getPartitions();
408 if (maxZones == null || maxZones < 1) {
409 this.maxZones = panelType.getZones();
413 return new EvoCommunicator(ipAddress, tcpPort, ip150Password, pcPassword, scheduler, panelType,
414 maxPartitions, maxZones, useEncryption);
415 } catch (IOException e) {
416 logger.warn("Unable to create communicator for Panel={}. Message={}", panelType, e.getMessage());
417 throw new ParadoxRuntimeException(e);
422 public ICommunicatorBuilder withMaxZones(Integer maxZones) {
423 this.maxZones = maxZones;
428 public ICommunicatorBuilder withMaxPartitions(Integer maxPartitions) {
429 this.maxPartitions = maxPartitions;
434 public ICommunicatorBuilder withIp150Password(String ip150Password) {
435 this.ip150Password = ip150Password;
440 public ICommunicatorBuilder withPcPassword(String pcPassword) {
441 this.pcPassword = pcPassword;
446 public ICommunicatorBuilder withIpAddress(String ipAddress) {
447 this.ipAddress = ipAddress;
452 public ICommunicatorBuilder withTcpPort(Integer tcpPort) {
453 this.tcpPort = tcpPort;
458 public ICommunicatorBuilder withScheduler(ScheduledExecutorService scheduler) {
459 this.scheduler = scheduler;
464 public ICommunicatorBuilder withEncryption(boolean useEncryption) {
465 this.useEncryption = useEncryption;