]> git.basschouten.com Git - openhab-addons.git/blob
865f3609fceec3d7482c08b1702ec3702b696729
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.paradoxalarm.internal.communication;
14
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;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
25
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;
39
40 /**
41  * The {@link EvoCommunicator} is responsible for handling communication to Evo192 alarm system via IP150 interface.
42  *
43  * @author Konstantin Polihronov - Initial contribution
44  */
45 public class EvoCommunicator extends GenericCommunicator implements IParadoxCommunicator {
46
47     private static final byte RAM_BLOCK_SIZE = (byte) 64;
48
49     private final Logger logger = LoggerFactory.getLogger(EvoCommunicator.class);
50
51     private MemoryMap memoryMap;
52
53     private Map<EntityType, Map<Integer, String>> entityLabelsMap = new HashMap<>();
54
55     private PanelType panelType = PanelType.UNKNOWN;
56     private Integer maxPartitions;
57     private Integer maxZones;
58
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();
68     }
69
70     @Override
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);
78         } else {
79             logger.debug("Wrong parsed result. Probably wrong data received in response. Response={}", response);
80             return;
81         }
82     }
83
84     @Override
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));
93             }
94
95             // Trigger listeners update when last memory page update is received
96             if (ramBlockNumber == panelType.getRamPagesNumber()) {
97                 updateListeners();
98             }
99         } else {
100             logger.debug("Wrong parsed result. Probably wrong data received in response");
101             return;
102         }
103     }
104
105     private void updateEntityLabel(EntityType entityType, int entityId, byte[] payload) {
106         String label = createString(payload);
107         logger.debug("{} label updated to: {}", entityType, label);
108
109         entityLabelsMap.get(entityType).put(entityId, label);
110     }
111
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;
116
117         try {
118             IPayload payload = new EpromRequestPayload(address, labelLength);
119             ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
120
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());
125         }
126     }
127
128     private ParadoxIPPacket createSerialPassthroughPacket(IPayload payload) {
129         ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes());
130         packet.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST).setUnknown0((byte) 0x14);
131         return packet;
132     }
133
134     private void retrieveZoneLabel(int zoneNumber) {
135         logger.debug("Submitting request for zone label: {}", zoneNumber);
136         final byte labelLength = 16;
137
138         try {
139             int address;
140             if (zoneNumber < 96) {
141                 address = 0x430 + (zoneNumber) * 16;
142             } else {
143                 address = 0x62F7 + (zoneNumber - 96) * 16;
144             }
145
146             IPayload payload = new EpromRequestPayload(address, labelLength);
147             ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
148
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());
153         }
154     }
155
156     @Override
157     public List<byte[]> getPartitionFlags() {
158         List<byte[]> result = new ArrayList<>();
159
160         byte[] element = memoryMap.getElement(2);
161         byte[] firstBlock = Arrays.copyOfRange(element, 32, 64);
162
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));
168         }
169
170         return result;
171     }
172
173     @Override
174     public ZoneStateFlags getZoneStateFlags() {
175         ZoneStateFlags result = new ZoneStateFlags();
176
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);
182
183         createSpecialZoneFlags(result, memoryMap);
184
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());
189
190         return result;
191     }
192
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.isBigRamEvo(panelType)) {
197             result.setZonesOpened(firstBlock);
198         } else {
199             byte[] secondBlock = Arrays.copyOfRange(secondPage, 0, 12);
200             byte[] zonesOpened = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
201             result.setZonesOpened(zonesOpened);
202         }
203     }
204
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.isBigRamEvo(panelType)) {
209             result.setZonesTampered(firstBlock);
210         } else {
211             byte[] secondBlock = Arrays.copyOfRange(secondPage, 12, 24);
212             byte[] zonesTampered = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
213             result.setZonesTampered(zonesTampered);
214         }
215     }
216
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.isBigRamEvo(panelType)) {
221             result.setZonesLowBattery(firstBlock);
222         } else {
223             byte[] secondBlock = Arrays.copyOfRange(secondPage, 24, 36);
224             byte[] zonesLowBattery = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
225             result.setZonesLowBattery(zonesLowBattery);
226         }
227     }
228
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);
236
237         switch (panelType) {
238             case EVO48:
239                 byte[] firstBlock = Arrays.copyOfRange(page2, 0, 48);
240                 result.setZoneSpecialFlags(firstBlock);
241                 break;
242             case EVO96:
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);
247                 break;
248             case EVO192:
249             case EVOHD:
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,
256                         fifthBlock);
257                 result.setZoneSpecialFlags(specialZoneFlags);
258                 break;
259         }
260     }
261
262     public void initializeMemoryMap() {
263         for (EntityType type : EntityType.values()) {
264             entityLabelsMap.put(type, new HashMap<>());
265         }
266
267         List<byte[]> ramCache = new ArrayList<>(panelType.getRamPagesNumber() + 1);
268         for (int i = 0; i <= panelType.getRamPagesNumber(); i++) {
269             ramCache.add(new byte[0]);
270         }
271         memoryMap = new MemoryMap(ramCache);
272     }
273
274     @Override
275     public void refreshMemoryMap() {
276         if (!isOnline()) {
277             logger.debug("Attempt to refresh memory map was made, but communicator is not online. Skipping update.");
278             return;
279         }
280
281         SyncQueue queue = SyncQueue.getInstance();
282         synchronized (queue) {
283             for (int i = 1; i <= panelType.getRamPagesNumber(); i++) {
284                 submitRamRequest(i);
285             }
286         }
287     }
288
289     private void submitRamRequest(int blockNo) {
290         try {
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) {
297             logger.debug(
298                     "Unable to create request payload from provided bytes to read. blockNo={}, bytes to read={}. Exception={}",
299                     blockNo, RAM_BLOCK_SIZE, e.getMessage());
300         }
301     }
302
303     private String createString(byte[] payloadResult) {
304         return new String(payloadResult, StandardCharsets.US_ASCII);
305     }
306
307     @Override
308     public void executeCommand(String command) {
309         IP150Command ip150Command = IP150Command.valueOf(command);
310         switch (ip150Command) {
311             case LOGIN:
312                 startLoginSequence();
313                 return;
314             case LOGOUT:
315                 CommunicationState.LOGOUT.runPhase(this);
316                 return;
317             case RESET:
318                 CommunicationState.LOGOUT.runPhase(this);
319                 scheduler.schedule(this::startLoginSequence, 5, TimeUnit.SECONDS);
320                 return;
321             default:
322                 logger.debug("Command {} not implemented.", command);
323         }
324     }
325
326     @Override
327     public MemoryMap getMemoryMap() {
328         return memoryMap;
329     }
330
331     public Map<EntityType, Map<Integer, String>> getEntityLabelsMap() {
332         return entityLabelsMap;
333     }
334
335     @Override
336     public Map<Integer, String> getPartitionLabels() {
337         return entityLabelsMap.get(EntityType.PARTITION);
338     }
339
340     @Override
341     public Map<Integer, String> getZoneLabels() {
342         return entityLabelsMap.get(EntityType.ZONE);
343     }
344
345     @Override
346     public void initializeData() {
347         synchronized (SyncQueue.getInstance()) {
348             initializeEpromData();
349             refreshMemoryMap();
350         }
351     }
352
353     private void initializeEpromData() {
354         for (int i = 0; i < maxPartitions; i++) {
355             retrievePartitionLabel(i);
356         }
357         for (int i = 0; i < maxZones; i++) {
358             retrieveZoneLabel(i);
359         }
360     }
361
362     public static class EvoCommunicatorBuilder implements ICommunicatorBuilder {
363
364         private final Logger logger = LoggerFactory.getLogger(EvoCommunicatorBuilder.class);
365
366         // Mandatory parameters
367         private PanelType panelType;
368         private String ipAddress;
369         private String ip150Password;
370         private ScheduledExecutorService scheduler;
371
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";
377
378         private boolean useEncryption;
379
380         EvoCommunicatorBuilder(PanelType panelType) {
381             this.panelType = panelType;
382         }
383
384         @Override
385         public IParadoxCommunicator build() {
386             if (ipAddress == null || ipAddress.isEmpty()) {
387                 final String msg = "IP address cannot be empty !";
388                 logger.debug(msg);
389                 throw new ParadoxRuntimeException(msg);
390             }
391
392             if (ip150Password == null || ip150Password.isEmpty()) {
393                 final String msg = "Password for IP150 cannot be empty !";
394                 logger.debug(msg);
395                 throw new ParadoxRuntimeException(msg);
396             }
397
398             if (scheduler == null) {
399                 final String msg = "Scheduler is mandatory parameter !";
400                 logger.debug(msg);
401                 throw new ParadoxRuntimeException(msg);
402             }
403
404             if (maxPartitions == null || maxPartitions < 1) {
405                 this.maxPartitions = panelType.getPartitions();
406             }
407
408             if (maxZones == null || maxZones < 1) {
409                 this.maxZones = panelType.getZones();
410             }
411
412             try {
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);
418             }
419         }
420
421         @Override
422         public ICommunicatorBuilder withMaxZones(Integer maxZones) {
423             this.maxZones = maxZones;
424             return this;
425         }
426
427         @Override
428         public ICommunicatorBuilder withMaxPartitions(Integer maxPartitions) {
429             this.maxPartitions = maxPartitions;
430             return this;
431         }
432
433         @Override
434         public ICommunicatorBuilder withIp150Password(String ip150Password) {
435             this.ip150Password = ip150Password;
436             return this;
437         }
438
439         @Override
440         public ICommunicatorBuilder withPcPassword(String pcPassword) {
441             this.pcPassword = pcPassword;
442             return this;
443         }
444
445         @Override
446         public ICommunicatorBuilder withIpAddress(String ipAddress) {
447             this.ipAddress = ipAddress;
448             return this;
449         }
450
451         @Override
452         public ICommunicatorBuilder withTcpPort(Integer tcpPort) {
453             this.tcpPort = tcpPort;
454             return this;
455         }
456
457         @Override
458         public ICommunicatorBuilder withScheduler(ScheduledExecutorService scheduler) {
459             this.scheduler = scheduler;
460             return this;
461         }
462
463         @Override
464         public ICommunicatorBuilder withEncryption(boolean useEncryption) {
465             this.useEncryption = useEncryption;
466             return this;
467         }
468     }
469 }