001/*
002 * geordi
003 *
004 * Copyright (C) 2018 Richard "Shred" Körber
005 *   https://github.com/shred/geordi
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
015 */
016package org.shredzone.geordi.device;
017
018import java.io.IOException;
019import java.io.InputStreamReader;
020import java.io.Reader;
021import java.math.BigDecimal;
022import java.net.URL;
023import java.time.Instant;
024import java.util.List;
025import java.util.stream.Collectors;
026
027import javax.inject.Inject;
028
029import org.json.JSONObject;
030import org.shredzone.commons.xml.XQuery;
031import org.shredzone.geordi.GeordiException;
032import org.shredzone.geordi.data.Sample;
033import org.shredzone.geordi.sensor.Sensor;
034import org.shredzone.geordi.service.DatabaseService;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * A {@link Device} implementation that reads values from Homematic CCU2 home automation
040 * centrals.
041 * <p>
042 * Requires the <a href="https://github.com/hobbyquaker/XML-API">XML-API CCU addon</a> to
043 * be installed on the CCU2.
044 *
045 * @see <a href="https://www.eq-3.de/">eQ-3 AG</a>
046 */
047public class Ccu2Device extends Device {
048
049    private final Logger log = LoggerFactory.getLogger(getClass());
050
051    @Inject
052    private DatabaseService databaseService;
053
054    @Override
055    public List<Sample> readSensors() {
056        XQuery values = fetchFromServer();
057
058        return databaseService.fetchSensors(this).stream()
059                .map(sensor -> getSensorValue(values, sensor))
060                .filter(it -> it != null)
061                .collect(Collectors.toList());
062    }
063
064    /**
065     * Reads the current sensor value from the given {@link Sensor}.
066     *
067     * @param values
068     *            XML that was read from the CCU2
069     * @param sensor
070     *            {@link Sensor} to be read
071     * @return {@link Sample} containing the sensor value
072     */
073    private Sample getSensorValue(XQuery values, Sensor sensor) {
074        try {
075            JSONObject config = sensor.getConfig();
076
077            XQuery sensorDevice;
078
079            if (config.has("datapointName")) {
080                sensorDevice = values.get(String.format(
081                        "//datapoint[@name='%s']",
082                        config.getString("datapointName")));
083            } else if (config.has("datapointId")) {
084                sensorDevice = values.get(String.format(
085                        "/stateList/device[@ise_id='%d']/channel[@ise_id='%d']/datapoint[@ise_id='%d']",
086                        config.getInt("deviceId"),
087                        config.getInt("channelId"),
088                        config.getInt("datapointId")));
089            } else {
090                sensorDevice = values.get(String.format(
091                        "/stateList/device[@ise_id='%d']/channel[@ise_id='%d']/datapoint[@type='%s']",
092                        config.getInt("deviceId"),
093                        config.getInt("channelId"),
094                        config.getString("type")));
095            }
096
097            String timestampStr = sensorDevice.attr().get("timestamp");
098            Instant timestamp = Instant.ofEpochMilli(Long.parseLong(timestampStr) * 1000L);
099
100            String valueStr = sensorDevice.attr().get("value");
101            BigDecimal value;
102            if ("false".equals(valueStr)) {
103                value = BigDecimal.ZERO;
104            } else if ("true".equals(valueStr)) {
105                value = BigDecimal.ONE;
106            } else {
107                value = new BigDecimal(valueStr);
108            }
109
110            return new Sample(sensor, timestamp, value);
111        } catch (Exception ex) {
112            log.warn("Could not read sensor id {} ({})", sensor.getId(), sensor.getName(), ex);
113            return null;
114        }
115    }
116
117    /**
118     * Reads the current status from the CCU2.
119     *
120     * @return XML containing the sensor status
121     */
122    private XQuery fetchFromServer() {
123        try {
124            URL url = new URL("http://"
125                            + getConfig().getString("host")
126                            + "/addons/xmlapi/statelist.cgi");
127
128            try (Reader in = new InputStreamReader(url.openStream(), "iso-8859-1")) {
129                return XQuery.parse(in);
130            }
131        } catch (IOException ex) {
132            throw new GeordiException("Could not read CCU2", ex);
133        }
134    }
135
136}