avocado goes „Smart Office“

smart office

Wir als Unternehmen haben uns das Ziel gesetzt, durch Smart-Geräte den Energiebedarf unserer Büros langfristig zu senken.

Den Anfang machen die Switches von myStrom, die den Stromverbrauch an einer Steckdose messen und den Strom über ein Relais schalten können.

Die Aufgabe besteht nun darin die Daten der Switches zu sammeln und auszuwerten. Dafür entwickeln wir eine Webapplikation, welche uns die Daten der Switches sammelt und grafisch aufbereitet. Des Weiteren wollen wir einen Gamification Faktor schaffen indem wir die Geräte in Gruppen einteilen, um diese dann vergleichen zu können. Dadurch sollen unsere Mitarbeiter motiviert werden Strom beziehungsweise Energie einzusparen.

Einführung

Wir als Unternehmen haben uns das Ziel gesetzt, durch Smart-Geräte den Energiebedarf unserer Büros langfristig zu senken.

Den Anfang machen die Switches von myStrom, die den Stromverbrauch an einer Steckdose messen und den Strom über ein Relais schalten können.

Die Aufgabe besteht nun darin die Daten der Switches zu sammeln und auszuwerten. Dafür entwickeln wir eine Webapplikation, welche uns die Daten der Switches sammelt und grafisch aufbereitet. Des Weiteren wollen wir einen Gamification Faktor schaffen indem wir die Geräte in Gruppen einteilen, um diese dann vergleichen zu können. Dadurch sollen unsere Mitarbeiter motiviert werden Strom beziehungsweise Energie einzusparen.

Warum Switches von myStrom?

Auf der Suche nach smarten Geräten mit einer gut dokumentierten Schnittstelle sind wir auf die Geräte von myStrom gestoßen, welche uns für unsere Anforderungen eine gute API bieten.

Smart-Office Portal

Das Dashboard

Das Dashboard soll als Übersicht für alle Bereiche des Smart-Office Portals dienen. Im Fall der Switches bewegen wir uns im Bereich Strom, denkbar wäre auch Temperatur oder Helligkeit. Im Strombereich wird in einem gestapelten Balkendiagramm der Gesamtverbrauch pro Tag oder Stunde angezeigt. Jede Gruppe wird mit ihrem Anteil am Gesamtverbrauch anteilsmäßig in einem Balken dargestellt. Bei Änderung des Zeitraumes, werden die angezeigten Daten angepasst.

Devices & Groups

Auf der Geräteübersichtsseite können wir Gruppen erstellen. In diesen Gruppen können wir dann Geräte hinzufügen.

Die Gerätedetails

Auf der Detailseite eines Gerätes werden einige Daten zum Gerät selbst, aktuelle Daten sowie Langzeitdaten dargestellt. Die Langzeitdaten ändern sich je nach ausgewähltem Zeitraum.

Technische Umsetzung

Daten – abfragen

@Service
public class RestClient {

    private final Logger logger;
    private final WebClient client;

    public RestClient(final Logger logger) {
        this.logger = logger;
        this.client = WebClient.create();
    }

    public Mono<MyStromSwitchReport> getReport(final String ip) {

        logger.info("RestCLient.getReport({})", ip);

        return client.get()
                .uri("http://{ip}/report", ip)
                .retrieve()
                .bodyToMono(MyStromSwitchReport.class);
    }
}

Um die Daten der Switches abzufragen benutzen wir einen Webclient. Die Switches bieten uns über ihre API folgenden Report, welchen wir in der Datenbank speichern.

{
    "power": 231.52302551269531
    "Ws": 248.57005310058594,
    "relay": true,
    "temperature": 22.983306884765625
}

power: Aktueller Stromverbrauch

Ws: Durchschnittlicher Stromverbrauch seit der letzten Abfrage

relay: Aktueller Zustand des internen Relais

temperatur: Aktuelle Temperatur in der Nähe der switch. Besonders der letzte Wert ist für unser Monitoring interessant, da wir damit den durchschnittlichen Stromverbrauch tracken können.

Daten – sammeln

Damit wir kontinuierlich und automatisiert Daten sammeln, ohne dies selbst erledigen zu müssen, haben wir uns eine automatische Abfrage gebaut.

cron:
  expression: "0 0/15 * * * *"

Dafür haben wir in den Applikationseigenschaften eine „Cron Expression“ angelegt, die für jedes Viertel einer Stunde gilt.

@Scheduled(cron = "${cron.expression}")
public void getDataScheduled() {

    logger.info("MyStromSwitchDataHandler.getDataScheduled()");

    List<MyStromSwitch> switches = switchMapper.findAll();

    Map<Integer, MyStromSwitchReport> switchReportMap = new HashMap<>();

    switches.forEach(s -> {
        try {
            MyStromSwitchReport data = this.restClient.getReport(
                    s.getSwitchIp()).block();
            switchReportMap.put(s.getId(), data);
        } catch ( Exception e) {
            logger.error("MyStromSwitchDataHandler.getDataScheduled(): {}"
                    , e.getMessage());
        }

    });

    switchDataMapper.insertFromMap(switchReportMap);
}

Die Cron Expression wird dafür genutzt, um die Funktion aufzurufen, welche die Reportdaten aller registrierten Switches in einer HashMap sammelt.

// Hinzufügen der SwitchDatas als Liste von Records -> 1xDatenbankzugriff
public final void insertFromMap(Map<Integer,MyStromSwitchReport> reportMap) {
    logger.info("MyStromSwitchDataMapper.insertFromMap()");

    List<MystromSwitchesDataRecord> dataRecordList = new ArrayList<MystromSwitchesDataRecord>();

    reportMap.forEach( (k, v) -> {
        MystromSwitchesDataRecord dataRecord = dslContext.newRecord(MYSTROM_SWITCHES_DATA);
        dataRecord.setSwitcheIdFk(k);
        dataRecord.setPower(v.getPower());
        dataRecord.setRelay(v.numRelay());
        dataRecord.setWs(v.getWs());
        dataRecord.setTemperature(v.getTemperature());

        dataRecordList.add(dataRecord);
    });

    dslContext.batchInsert(dataRecordList).execute();
}

Zum Schluss werden die gesammelten Reports in einem Batchlauf in die Datenbank geschrieben.

Daten – zusammenführen

public final List<MyStromSwitchData> getGroupedGroupData(final DataPeriod period, final String granularity) {

    logger.info("MyStromSwitchDataMapper.getGroupedGroupData()");

    double factor = getFactor(granularity);

    SelectConditionStep<Record5<Timestamp, String, BigDecimal, BigDecimal, Integer>>
            baseQuery = dslContext
                    .select(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP
                                    .as("timestamp"),
                            value(granularity)
                                    .as("granularity"),
                            (avg(Tables.MYSTROM_SWITCHES_DATA.WS)
                                    .mul(factor))
                                    .as("avgPower"),
                            (avg(Tables.MYSTROM_SWITCHES_DATA.TEMPERATURE))
                                    .as("avgTemp"),
                            Tables.SWITCHES_IN_GROUPS.SMART_GROUP_ID
                                    .as("groupId"))
                    .from(Tables.MYSTROM_SWITCHES_DATA)
                    .join(Tables.SWITCHES_IN_GROUPS)
                    .on(Tables.SWITCHES_IN_GROUPS.SWITCH_ID
                            .eq(Tables.MYSTROM_SWITCHES_DATA.SWITCHE_ID_FK))
                    .where(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP
                            .ge(new Timestamp(period.getStartDate().getTime())))
                    .and(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP
                            .le(new Timestamp(period.getEndDate().getTime())))
                    .andNot(Tables.MYSTROM_SWITCHES_DATA.WS
                            .eq(BigDecimal.valueOf(0)));
                    .on(Tables.SWITCHES_IN_GROUPS.SWITCH_ID.eq(Tables.MYSTROM_SWITCHES_DATA.SWITCHE_ID_FK))
                    .where(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP
                            .ge(new Timestamp(period.getStartDate().getTime())))
                    .and(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP
                            .le(new Timestamp(period.getEndDate().getTime())))
                    .andNot(Tables.MYSTROM_SWITCHES_DATA.WS
                            .eq(BigDecimal.valueOf(0)));

    if (granularity.equals("hour")) {
        return baseQuery.groupBy(day(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP),
                hour(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP),
                Tables.SWITCHES_IN_GROUPS.SWITCH_ID,
                Tables.SWITCHES_IN_GROUPS.SMART_GROUP_ID)
                .fetchInto(MyStromSwitchData.class);

    } else {
        return baseQuery.groupBy(day(Tables.MYSTROM_SWITCHES_DATA.TIMESTAMP),
                Tables.SWITCHES_IN_GROUPS.SWITCH_ID,
                Tables.SWITCHES_IN_GROUPS.SMART_GROUP_ID)
                .fetchInto(MyStromSwitchData.class);
    }
}

Je nach Zeitraum den wir in unserem User Interface gewählt haben, wird die Granularität (Stunde oder Tag) der geforderten Daten berechnet. Mit dieser Granularität und dem Zeitraum werden die Daten gruppiert und zusammengefasst von der Datenbank abgefragt und an unsere UI gesendet.


„Das interne Praxisprojekt selbstständig, von Null auf zu bearbeiten war die perfekte Herausforderung um mir Kenntnisse als Full Stack Developer anzueignen. Ich hoffe es werden weitere Projekte daraus folgen.“

Philipp S. (Student Wirtschaftsinformatik, 6.Semester)

Veröffentlicht am 13. Juli 2021