######################################################################### # $Id$ # fhem Modul für Vissmann API. Based on investigation of "thetrueavatar" # (https://github.com/thetrueavatar/Viessmann-Api) # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## # Changelog: # # 2018-11-24 initial version # 2018-12-11 non-blocking # Reading "status" in "state" umbenannt # 2018-12-23 Neue Werte in der API werden unter ihrem JSON Name als Reading eingetragen # Neue Readings: # heating.boiler.sensors.temperature.commonSupply.status error # heating.boiler.temperature.value 48.1 # heating.burner.modulation.value 11 # heating.burner.statistics.hours 933.336666666667 # heating.burner.statistics.starts 2717 # heating.circuits.0.circulation.pump.status on # heating.dhw.charging.active 0 # heating.dhw.pumps.circulation.schedule.active 1 # heating.dhw.pumps.circulation.schedule.entries sun mode:on end:22:30 start:04:30 position:0, fri end:22:30 mode:on position:0 start:04:30, # mon mode:on end:22:30 start:04:30 position:0, # wed start:04:30 position:0 end:22:30 mode:on, thu mode:on end:22:30 position:0 start:04:30, sat end:22:30 mode:on position:0 start:04:30, # tue position:0 start:04:30 end:22:30 mode:on, # heating.dhw.pumps.circulation.status on # heating.dhw.pumps.primary.status off # heating.dhw.sensors.temperature.outlet.status error # heating.dhw.temperature.main.value 53 # 2018-12-30 initial offical release # remove special characters from readings # some internal improvements suggested by CoolTux # 2019-01-01 "disabled" implemented # "set update implemented # renamed "WW-onTimeCharge_aktiv" into "WW-einmaliges_Aufladen_aktiv" # Attribute vitoconnect_raw_readings:0,1 " and ."vitoconnect_actions_active:0,1 " implemented # "set clearReadings" implemented # 2019-01-05 Passwort wird im KeyValue gespeichert statt im Klartext # Action "oneTimeCharge" implemented # 2019-01-14 installation, code and gw in den Internals unsichtbar gemacht # Reading "counter" entfernt (ist weiterhin in Internals sichtbar) # Reading WW-einmaliges_Aufladen_active umbenannt in WW-einmaliges_Aufladen # Befehle zum setzen von # HK1-Betriebsart # HK2-Betriebsart # HK1-Solltemperatur_normal # HK2-Solltemperatur_normal # HK1-Solltemperatur_reduziert # HK2-Solltemperatur_reduziert # WW-einmaliges_Aufladen # Bedienfehler (z.B. Ausführung einer Befehls für HK2, wenn die Hezung nur einen Heizkreis hat) # führen zu einem "Bad Gateway" Fehlermeldung in Logfile # Achtung: Keine Prüfung ob Befehle sinnvoll und oder erlaubt sind! Nutzung auf eigene Gefahr! # 2019-01-15 Fehler bei der Befehlsausführung gefixt # # # ToDo: weitere "set"s zum Steuern der Heizung # Dokumentation (auch auf Deutsch) # Nicht bei jedem Lesen neu einloggen (wenn möglich) # Fehlerbehandlung verbessern # Attribute implementieren und dokumentieren # "sinnvolle" Readings statt 1:1 aus der API übernommene # ErrorListChanges implementieren # mapping der Readings optional machen # Mehrsprachigkeite # Auswerten der Reading in getCode usw. # package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use JSON; use HttpUtils; use Encode qw(decode encode); #use Data::Dumper; #use Path::Tiny; my $client_id = '79742319e39245de5f91d15ff4cac2a8'; my $client_secret = '8ad97aceb92c5892e102b093c7c083fa'; my $authorizeURL = 'https://iam.viessmann.com/idp/v1/authorize'; # my $token_url = 'https://iam.viessmann.com/idp/v1/token'; my $apiURLBase = 'https://api.viessmann-platform.io'; my $general = '/general-management/installations?expanded=true&'; my $callback_uri = "vicare://oauth-callback/everest"; my %RequestList = ( "heating.boiler.serial.value" => "Kessel_Seriennummer", "heating.boiler.temperature.value" => "Kesseltemperatur_exact", "heating.boiler.sensors.temperature.commonSupply.status" => "Kessel_Common_Supply", "heating.boiler.sensors.temperature.main.status" => "Kessel_Status", "heating.boiler.sensors.temperature.main.value" => "Kesseltemperatur", "heating.burner.active" => "Brenner_aktiv", "heating.burner.automatic.status" => "Brenner_Status", "heating.burner.automatic.errorCode" => "Brenner_Fehlercode", "heating.burner.current.power.value" => "Brenner_Leistung", "heating.burner.modulation.value" => "Brenner_Modulation", "heating.burner.statistics.hours" => "Brenner_Beriebsstunden", "heating.burner.statistics.starts" => "Brenner_Starts", "heating.circuits.enabled" => "Aktive_Heizkreise", "heating.circuits.0.active" => "HK1-aktiv", "heating.circuits.0.circulation.pump.status" => "HK1-Zirkulationspumpe", "heating.circuits.0.circulation.schedule.active" => "HK1-Zeitsteuerung_Zirkulation_aktiv", "heating.circuits.0.circulation.schedule.entries" => "HK1-Zeitsteuerung_Zirkulation", "heating.circuits.0.frostprotection.status" => "HK1-Frostschutz_Status", "heating.circuits.0.heating.curve.shift" => "HK1-Heizkurve-Niveau", "heating.circuits.0.heating.curve.slope" => "HK1-Heizkurve-Steigung", "heating.circuits.0.heating.schedule.active" => "HK1-Zeitsteuerung_Heizung_aktiv", "heating.circuits.0.heating.schedule.entries" => "HK1-Zeitsteuerung_Heizung", "heating.circuits.0.operating.modes.active.value" => "HK1-Betriebsart", "heating.circuits.0.operating.modes.dhw.active" => "HK1-WW_aktiv", "heating.circuits.0.operating.modes.dhwAndHeating.active" => "HK1-WW_und_Heizen_aktiv", "heating.circuits.0.operating.modes.forcedNormal.active" => "HK1-Solltemperatur_erzwungen", "heating.circuits.0.operating.modes.forcedReduced.active" => "HK1-Reduzierte_Temperatur_erzwungen", "heating.circuits.0.operating.modes.standby.active" => "HK1-Standby_aktiv", "heating.circuits.0.operating.programs.active.value" => "HK1-Programmstatus", "heating.circuits.0.operating.programs.comfort.active" => "HK1-Solltemperatur_comfort_aktiv", "heating.circuits.0.operating.programs.comfort.temperature" => "HK1-Solltemperatur_comfort", "heating.circuits.0.operating.programs.eco.active" => "HK1-Solltemperatur_eco_aktiv", "heating.circuits.0.operating.programs.eco.temperature" => "HK1-Solltemperatur_eco", "heating.circuits.0.operating.programs.external.active" => "HK1-External_aktiv", "heating.circuits.0.operating.programs.external.temperature" => "HK1-External_Temperatur", "heating.circuits.0.operating.programs.holiday.active" => "HK1-Urlaub_aktiv", "heating.circuits.0.operating.programs.holiday.start" => "HK1-Urlaub_Start", "heating.circuits.0.operating.programs.holiday.end" => "HK1-Urlaub_Ende", "heating.circuits.0.operating.programs.normal.active" => "HK1-Solltemperatur_aktiv", "heating.circuits.0.operating.programs.normal.temperature" => "HK1-Solltemperatur_normal", "heating.circuits.0.operating.programs.reduced.active" => "HK1-Solltemperatur_reduziert_aktiv", "heating.circuits.0.operating.programs.reduced.temperature" => "HK1-Solltemperatur_reduziert", "heating.circuits.0.operating.programs.standby.active" => "HK1-Standby_aktiv", "heating.circuits.0.sensors.temperature.room.status" => "HK1-Raum_Status", "heating.circuits.0.sensors.temperature.supply.status" => "HK1-Vorlauftemperatur_aktiv", "heating.circuits.0.sensors.temperature.supply.value" => "HK1-Vorlauftemperatur", "heating.circuits.1.active" => "HK2-aktiv", "heating.circuits.1.circulation.pump.status" => "HK2-Zirkulationspumpe", "heating.circuits.1.circulation.schedule.active" => "HK2-Zeitsteuerung_Zirkulation_aktiv", "heating.circuits.1.circulation.schedule.entries" => "HK2-Zeitsteuerung_Zirkulation", "heating.circuits.1.frostprotection.status" => "HK2-Frostschutz_Status", "heating.circuits.1.heating.curve.shift" => "HK2-Heizkurve-Niveau", "heating.circuits.1.heating.curve.slope" => "HK2-Heizkurve-Steigung", "heating.circuits.1.heating.schedule.active" => "HK2-Zeitsteuerung_Heizung_aktiv", "heating.circuits.1.heating.schedule.entries" => "HK2-Zeitsteuerung_Heizung", "heating.circuits.1.operating.modes.active.value" => "HK2-Betriebsart", "heating.circuits.1.operating.modes.dhw.active" => "HK2-WW_aktiv", "heating.circuits.1.operating.modes.dhwAndHeating.active" => "HK2-WW_und_Heizen_aktiv", "heating.circuits.1.operating.modes.forcedNormal.active" => "HK2-Solltemperatur_erzwungen", "heating.circuits.1.operating.modes.forcedReduced.active" => "HK2-Reduzierte_Temperatur_erzwungen", "heating.circuits.1.operating.modes.standby.active" => "HK2-Standby_aktiv", "heating.circuits.1.operating.programs.active.value" => "HK2-Programmstatus", "heating.circuits.1.operating.programs.comfort.active" => "HK2-Solltemperatur_comfort_aktiv", "heating.circuits.1.operating.programs.comfort.temperature" => "HK2-Solltemperatur_comfort", "heating.circuits.1.operating.programs.eco.active" => "HK2-Solltemperatur_eco_aktiv", "heating.circuits.1.operating.programs.eco.temperature" => "HK2-Solltemperatur_eco", "heating.circuits.1.operating.programs.external.active" => "HK2-External_aktiv", "heating.circuits.1.operating.programs.external.temperature" => "HK2-External_Temperatur", "heating.circuits.1.operating.programs.holiday.active" => "HK2-Urlaub_aktiv", "heating.circuits.1.operating.programs.holiday.start" => "HK2-Urlaub_Start", "heating.circuits.1.operating.programs.holiday.end" => "HK2-Urlaub_Ende", "heating.circuits.1.operating.programs.normal.active" => "HK2-Solltemperatur_aktiv", "heating.circuits.1.operating.programs.normal.temperature" => "HK2-Solltemperatur_normal", "heating.circuits.1.operating.programs.reduced.active" => "HK2-Solltemperatur_reduziert_aktiv", "heating.circuits.1.operating.programs.reduced.temperature" => "HK2-Solltemperatur_reduziert", "heating.circuits.1.operating.programs.standby.active" => "HK2-Standby_aktiv", "heating.circuits.1.sensors.temperature.room.status" => "HK2-Raum_Status", "heating.circuits.1.sensors.temperature.supply.status" => "HK2-Vorlauftemperatur_aktiv", "heating.circuits.1.sensors.temperature.supply.value" => "HK2-Vorlauftemperatur", "heating.configuration.multiFamilyHouse.active" => "Mehrfamilenhaus_aktiv", "heating.controller.serial.value" => "Controller_Seriennummer", "heating.device.time.offset.value" => "Device_Time_Offset", "heating.dhw.active" => "WW-aktiv", "heating.dhw.charging.active" => "WW-Aufladung", "heating.dhw.oneTimeCharge.active" => "WW-einmaliges_Aufladen", "heating.dhw.pumps.circulation.schedule.active" => "WW-Zirklationspumpe_Zeitsteuerung_aktiv", "heating.dhw.pumps.circulation.schedule.entries" => "WW-Zirkulationspumpe_Zeitplan", "heating.dhw.pumps.circulation.status" => "WW-Zirkulationspumpe_Status", "heating.dhw.pumps.primary.status" => "WW-Zirkulationspumpe_primaer", "heating.dhw.sensors.temperature.outlet.status" => "WW-Sensoren_Auslauf_Status", "heating.dhw.sensors.temperature.outlet.value" => "WW-Sensoren_Auslauf_Wert", "heating.dhw.temperature.main.value" => "WW-Haupttemperatur", "heating.dhw.sensors.temperature.hotWaterStorage.status" => "WW-Temperatur_aktiv", "heating.dhw.sensors.temperature.hotWaterStorage.value" => "WW-Isttemperatur", "heating.dhw.temperature.value" => "WW-Solltemperatur", "heating.dhw.schedule.active" => "WW-zeitgesteuert_aktiv", "heating.dhw.schedule.entries" => "WW-Zeitplan", "heating.errors.active.entries" => "Fehlereintraege_aktive", "heating.errors.history.entries" => "Fehlereintraege_Historie", "heating.gas.consumption.dhw.day" => "Gasverbrauch_WW/Tag", "heating.gas.consumption.dhw.week" => "Gasverbrauch_WW/Woche", "heating.gas.consumption.dhw.month" => "Gasverbrauch_WW/Monat", "heating.gas.consumption.dhw.year" => "Gasverbrauch_WW/Jahr", "heating.gas.consumption.heating.day" => "Gasverbrauch_Heizung/Tag", "heating.gas.consumption.heating.week" => "Gasverbrauch_Heizung/Woche", "heating.gas.consumption.heating.month" => "Gasverbrauch_Heizung/Monat", "heating.gas.consumption.heating.year" => "Gasverbrauch_Heizung/Jahr", "heating.sensors.temperature.outside.status" => "Aussen_Status", "heating.sensors.temperature.outside.statusWired" => "Aussen_StatusWired", "heating.sensors.temperature.outside.statusWireless" => "Aussen_StatusWireless", "heating.sensors.temperature.outside.value" => "Aussentemperatur", "heating.service.timeBased.serviceDue" => "Service_faellig", "heating.service.timeBased.serviceIntervalMonths" => "Service_Intervall_Monate", "heating.service.timeBased.activeMonthSinceLastService" => "Service_Monate_aktiv_seit_letzten_Service", "heating.service.timeBased.lastService" => "Service_Letzter", "heating.service.burnerBased.serviceDue" => "Service_fällig_brennerbasiert", "heating.service.burnerBased.serviceIntervalBurnerHours" => "Service_Intervall_Betriebsstunden", "heating.service.burnerBased.activeBurnerHoursSinceLastService" => "Service_Betriebsstunden_seit_letzten", "heating.service.burnerBased.lastService" => "Service_Letzter_brennerbasiert" ); sub vitoconnect_Initialize($) { my ($hash) = @_; $hash->{DefFn} = 'vitoconnect_Define'; $hash->{UndefFn} = 'vitoconnect_Undef'; $hash->{SetFn} = 'vitoconnect_Set'; $hash->{GetFn} = 'vitoconnect_Get'; $hash->{AttrFn} = 'vitoconnect_Attr'; $hash->{ReadFn} = 'vitoconnect_Read'; $hash->{AttrList} = "disable:0,1 " ."vitoconnect_raw_readings:0,1 " ."vitoconnect_actions_active:0,1 " .$readingFnAttributes; } sub vitoconnect_Define($$) { my ($hash, $def) = @_; my $name = $hash->{NAME}; my @param = split('[ \t]+', $def); if(int(@param) < 5) { return "too few parameters: define vitoconnect "; } $hash->{user} = $param[2]; $hash->{intervall} = $param[4]; $hash->{counter} = 0; my $err = vitoconnect_StoreKeyValue($hash, "passwd", $param[3]); return $err if ($err); InternalTimer(gettimeofday()+10, "vitoconnect_GetUpdate", $hash); return undef; } sub vitoconnect_Undef($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); return undef; } sub vitoconnect_Get($@) { my ($hash, $name, $opt, @args) = @_; return "get $name needs at least one argument" unless (defined($opt)); return undef; } sub vitoconnect_Set($@) { my ($hash, $name, $opt, @args) = @_; my $access_token = $hash->{".access_token"}; my $installation = $hash->{".installation"}; my $gw = $hash->{".gw"}; return "set $name needs at least one argument" unless (defined($opt)); if ($opt eq "update"){ vitoconnect_GetUpdate($hash); return undef; } elsif ($opt eq "clearReadings") { AnalyzeCommand ($hash, "deletereading $name .*"); return undef; } elsif ($opt eq "password") { my $err = vitoconnect_StoreKeyValue($hash, "passwd", $args[0]); return $err if ($err); return undef; } elsif ($opt eq "HK1-Heizkurve-Niveau") { return "not implemented"; } elsif ($opt eq "HK1-Heizkurve-Steigung") { return "not implemented"; } elsif ($opt eq "HeatingSchedule") { return "not implemented"; } elsif ($opt eq "HK1-Betriebsart") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.0.operating.modes.active/setMode", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"mode\":\"$args[0]\"}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne "" || defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data=$data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "HK2-Betriebsart") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.1.operating.modes.active/setMode", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"mode\":\"$args[0]\"}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne "" || defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data=$data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "HeadingModeComfort") { return "not implemented"; } elsif ($opt eq "TemperatureComfort") { return "not implemented"; } elsif ($opt eq "HeatingModeEco") { return "not implemented"; } elsif ($opt eq "HeatingHolidaySchedule") { return "not implemented"; } elsif ($opt eq "HeatingHolidayUnschedule") { return "not implemented"; } elsif ($opt eq "HK1-Solltemperatur_normal") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/v1/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.0.operating.programs.normal/setTemperature", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"targetTemperature\":$args[0]}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; #Log3 $name, 3, "$name: $param->{data}"; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne "" || defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data= $data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "HK2-Solltemperatur_normal") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/v1/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.1.operating.programs.normal/setTemperature", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"targetTemperature\":$args[0]}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; #Log3 $name, 3, "$name: $param->{data}"; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne "" || defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data= $data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "HK1-Solltemperatur_reduziert") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/v1/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.0.operating.programs.reduced/setTemperature", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"targetTemperature\":$args[0]}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne ""|| defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data=$data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "HK2-Solltemperatur_reduziert") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/v1/installations/$installation/gateways/$gw/devices/0/features/heating.circuits.1.operating.programs.reduced/setTemperature", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => "{\"targetTemperature\":$args[0]}", method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne ""|| defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: err= $err data=$data"; } else { Log3 $name, 4, "$name: Befehlsausführung ok"; } return undef; } elsif ($opt eq "WW-einmaliges_Aufladen") { vitoconnect_action($hash); my $param = { url => "https://api.viessmann-platform.io/operational-data/installations/$installation/gateways/$gw/devices/0/features/heating.dhw.oneTimeCharge/$args[0]", hash => $hash, header => "Authorization: Bearer $access_token\r\nContent-Type: application/json", data => '{}', method => "POST", timeout => 10, sslargs => {SSL_verify_mode => 0}, }; (my $err, my $data) = HttpUtils_BlockingGet($param); if ($err ne ""|| defined($data)) { Log3 $name, 1, "$name: Fehler während der Befehlsausführung: $err :: $data"; } else { Log3 $name, 5, "$name : Befehlsausführung ok"; } return undef; } elsif ($opt eq "WW-Zirkulationspumpe_Zeitplan") { return "not implemented"; } elsif ($opt eq "WW-ZeitplanDhwSchedule") { return "not implemented"; } elsif ($opt eq "WW-Haupttemperatur") { return "not implemented"; } elsif ($opt eq "WW-Solltemperatur") { return "not implemented"; } return "unknown value $opt, choose one of update:noArg clearReadings:noArg password " . # "HK1-Heizkurve-Niveau:slider,-13,1,40 ". # "HK1-Heizkurve-Steigung:slider,0.2,0.1,3.5,1 ". # "HeatingSchedule " . "HK1-Betriebsart:standby,dhw,dhwAndHeating,forcedReduced,forcedNormal " . "HK2-Betriebsart:standby,dhw,dhwAndHeating,forcedReduced,forcedNormal " . # "HeadingModeComfort:activate,deactivate " . # "TemperatureComfort:slider,4,1,37 " . # "HeatingModeEco:activate,deactivate " . # "HeatingHolidaySchedule " . # "HeatingHolidayUnschedule:NoArg " . "HK1-Solltemperatur_normal:slider,3,1,37 " . "HK2-Solltemperatur_normal:slider,3,1,37 " . "HK1-Solltemperatur_reduziert:slider,3,1,37 " . "HK2-Solltemperatur_reduziert:slider,3,1,37 " . "WW-einmaliges_Aufladen:activate,deactivate "; # . # "WW-Zirkulationspumpe_Zeitplan " . # "WW-Zeitplan " . #"WW-Haupttemperatur:slider,10,1,60 " . #"WW-Solltemperatur:slider,10,1,60 "; } sub vitoconnect_Attr(@) { my ($cmd,$name,$attr_name,$attr_value) = @_; if($cmd eq "set") { if($attr_name eq "vitoconnect_raw_readings") { if($attr_value !~ /^0|1$/) { my $err = "Invalid argument $attr_value to $attr_name. Must be 0 or 1."; Log 1, "$name: ".$err; return $err; } } elsif($attr_name eq "vitoconnect_actions_active") { if($attr_value !~ /^0|1$/) { my $err = "Invalid argument $attr_value to $attr_name. Must be 0 or 1."; Log 1, "$name: ".$err; return $err; } } elsif($attr_name eq "disable") { } elsif($attr_name eq "verbose") { } else { # return "Unknown attr $attr_name"; } } return undef; } # Subs sub vitoconnect_GetUpdate($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: GetUpdate called ..."; if ( IsDisabled($name) ) { Log3 $name, 4, "$name: device disabled"; InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); } else { vitoconnect_getCode($hash); } return undef; } sub vitoconnect_getCode($) { my ($hash) = @_; my $name = $hash->{NAME}; my $isiwebpasswd = vitoconnect_ReadKeyValue($hash, "passwd"); my $param = { url => "$authorizeURL?client_id=$client_id&scope=openid&redirect_uri=$callback_uri&response_type=code", hash => $hash, header => "Content-Type: application/x-www-form-urlencoded", ignoreredirects => 1, user => $hash->{user}, pwd => $isiwebpasswd, sslargs => {SSL_verify_mode => 0}, method => "POST", callback => \&vitoconnect_getCodeCallback }; # Log3 $name, 5, Dumper($hash); HttpUtils_NonblockingGet($param); return undef; } sub vitoconnect_getCodeCallback ($) { my ($param, $err, $response_body) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if ($err eq "") { Log3 $name, 4, "$name - getCodeCallback went ok"; Log3 $name, 5, "$name: Received response: $response_body"; $response_body =~ /code=(.*)"/; $hash->{".code"} = $1; Log3 $name, 5, "$name: code = " . $hash->{".code"}; if ($hash->{".code"}) { $hash->{login} = "ok"; } else { $hash->{login} = "failure"; } } else { # Error code, type of error, error message Log3 $name, 1, "$name: An error happened: $err"; $hash->{login} = "failure"; } if ($hash->{login} eq "ok") { vitoconnect_getAccessToken($hash); } else { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "state", "login failure" ); readingsEndUpdate($hash, 1); # neuen Timer starten in einem konfigurierten Interval. InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); } return undef; } sub vitoconnect_getAccessToken($) { my ($hash) = @_; my $name = $hash->{NAME}; my $param = { url => 'https://iam.viessmann.com/idp/v1/token', hash => $hash, header => "Content-Type: application/x-www-form-urlencoded;charset=utf-8", data => "client_id=$client_id&client_secret=$client_secret&code=" . $hash->{".code"} ."&redirect_uri=$callback_uri&grant_type=authorization_code", sslargs => {SSL_verify_mode => 0}, method => "POST", callback => \&vitoconnect_getAccessTokenCallback }; HttpUtils_NonblockingGet($param); return undef; } sub vitoconnect_getAccessTokenCallback($) { my ($param, $err, $response_body) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if ($err eq "") { Log3 $name, 4, "$name - getAccessTokenCallback went ok"; Log3 $name, 5, "$name: Received response: $response_body\n"; my $decode_json = eval{decode_json($response_body)}; if($@) { Log3 $name, 1, "$name - JSON error while request: $@"; return; } my $access_token = $decode_json->{"access_token"}; if ($access_token ne "") { $hash->{".access_token"} = $access_token; Log3 $name, 5, "$name: Access Token: $access_token"; vitoconnect_getGw($hash); } else { Log3 $name, 1, "$name: Access Token: undef"; InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); } } else { Log3 $name, 1, "$name: getAccessToken: An error happened: $err"; InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); } return undef; } sub vitoconnect_getGw($) { my ($hash) = @_; my $name = $hash->{NAME}; my $access_token = $hash->{".access_token"}; my $param = { url => "$apiURLBase$general", hash => $hash, header => "Authorization: Bearer $access_token", sslargs => {SSL_verify_mode => 0}, callback => \&vitoconnect_getGwCallback }; HttpUtils_NonblockingGet($param); return undef; } sub vitoconnect_getGwCallback($) { my ($param, $err, $response_body) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if ($err eq "") { Log3 $name, 4, "$name - getGwCallback went ok"; Log3 $name, 5, "$name: Received response: $response_body\n"; my $decode_json = eval{decode_json($response_body)}; if($@) { Log3 $name, 1, "$name - JSON error while request: $@"; return; } my $installation = $decode_json->{entities}[0]->{properties}->{id}; Log3 $name, 5, "$name: installation: $installation"; $hash->{".installation"} = $installation; my $gw = $decode_json->{entities}[0]->{entities}[0]->{properties}->{serial}; Log3 $name, 5, "$name gw: $gw"; $hash->{".gw"} = $gw; vitoconnect_getResource($hash); } else { Log3 $name, 1, "$name: An error happened: $err"; InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); } return undef; } sub vitoconnect_getResource($) { my ($hash) = @_; my $name = $hash->{NAME}; my $access_token = $hash->{".access_token"}; my $installation = $hash->{".installation"}; my $gw = $hash->{".gw"}; my $param = { url => "https://api.viessmann-platform.io/operational-data/installations/$installation/gateways/$gw/devices/0/features/", hash => $hash, header => "Authorization: Bearer $access_token", timeout => 10, sslargs => {SSL_verify_mode => 0}, callback => \&vitoconnect_getResourceCallback }; HttpUtils_NonblockingGet($param); return undef; } sub vitoconnect_getResourceCallback($) { my ($param, $err, $response_body) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if ($err eq "") { Log3 $name, 4, "$name - getResourceCallback went ok"; Log3 $name, 5, "Received response: $response_body\n"; my $decode_json = eval{decode_json($response_body)}; if($@) { Log3 $name, 1, "$name - JSON error while request: $@"; return; } my $items = $decode_json; ########################################### # my $dir = path("log"); # my $file = $dir->child("dumper.json"); # my $file_handle = $file->openw_utf8(); # $file_handle->print(Dumper($items)); #my $dir = path("log"); #my $file = $dir->child("actions.json"); #unlink($file); #my $file_handle = $file->opena_utf8(); ########################################### readingsBeginUpdate($hash); for my $item( @{$items->{entities}} ) { my $FieldName = $item->{class}[0]; Log3 $name, 5, "FieldName $FieldName"; my %Properties = %{$item->{properties}}; my @Keys = keys( %Properties ); for my $Key ( @Keys ) { my $Reading = $RequestList{$FieldName.".".$Key}; if ( !defined($Reading) || AttrVal($name,'vitoconnect_raw_readings',undef) eq "1" ) { $Reading = $FieldName.".".$Key; } # Log3 $name, 5, "Property: $FieldName $Key"; my $Type = $Properties{$Key}{type}; my $Value = $Properties{$Key}{value}; if ( $Type eq "string" ) { readingsBulkUpdate($hash, $Reading, $Value); Log3 $name, 5, "$FieldName".".$Key: $Value ($Type)"; } elsif ( $Type eq "number" ) { readingsBulkUpdate($hash, $Reading, $Value); Log3 $name, 5, "$FieldName".".$Key: $Value ($Type)"; } elsif ( $Type eq "array" ) { my $Array = join(",", @$Value); readingsBulkUpdate($hash, $Reading, $Array); Log3 $name, 5, "$FieldName".".$Key: $Array ($Type)"; } elsif ( $Type eq "boolean" ) { readingsBulkUpdate($hash, $Reading, $Value); Log3 $name, 5, "$FieldName".".$Key: $Value ($Type)"; } elsif ( $Type eq "Schedule" ) { my %Entries = %$Value; my @Days = keys (%Entries); my $Result = ""; for my $Day ( @Days ){ my $Entry = $Entries{$Day}; $Result = "$Result $Day"; for my $Element ( @$Entry ) { #$Result = "$Result $Element"; while(my($k, $v) = each %$Element) { $Result = "$Result $k:$v"; } $Result = "$Result, "; } } readingsBulkUpdate($hash, $Reading, $Result); Log3 $name, 5, "$FieldName".".$Key: $Result ($Type)"; } elsif ( $Type eq "ErrorListChanges" ) { # not implemented yet readingsBulkUpdate($hash, $Reading, "ErrorListChanges"); Log3 $name, 5, "$FieldName".".$Key: $Value ($Type)"; } else { readingsBulkUpdate($hash, $Reading, "Unknown: $Type"); Log3 $name, 5, "$FieldName".".$Key: $Value ($Type)"; } }; ########################################### if (AttrVal($name,'vitoconnect_actions_active',undef) eq "1" ) { my @actions = @{$item->{actions}}; if (@actions) { # $file_handle->print($FieldName."\n"); # $file_handle->print(Dumper(@actions)); #Log3 $name, 5, Dumper(@actions) for my $action (@actions) { my @fields = @{$action->{fields}}; my $Result = "action: "; for my $field (@fields) { $Result .= $field->{name}." "; } readingsBulkUpdate($hash, $FieldName.".".$action->{"name"}, $Result); } } } ########################################### }; #readingsBulkUpdate($hash, "counter", $hash->{".counter"} ); $hash->{counter} = $hash->{counter} + 1; readingsBulkUpdate($hash, "state", "ok"); } else { readingsBulkUpdate($hash, "state", "An error happened: $err"); Log3 $name, 1, "$name - An error happened: $err"; } readingsEndUpdate($hash, 1); InternalTimer(gettimeofday()+$hash->{intervall}, "vitoconnect_GetUpdate", $hash); return undef; } sub vitoconnect_action($) { my ($hash) = @_; my $name = $hash->{NAME}; my $isiwebuserid = $hash->{user}; my $isiwebpasswd = vitoconnect_ReadKeyValue($hash, "passwd"); my $err = ""; my $response_body = ""; my $code = ""; my $access_token = ""; my $installation = ""; my $gw = ""; my $param = { url => "$authorizeURL?client_id=$client_id&scope=openid&redirect_uri=$callback_uri&response_type=code", hash => $hash, header => "Content-Type: application/x-www-form-urlencoded", ignoreredirects => 1, user => $isiwebuserid, pwd => $isiwebpasswd, sslargs => {SSL_verify_mode => 0}, method => "POST" }; ($err, $response_body) = HttpUtils_BlockingGet($param); if ($err eq "") { $response_body =~ /code=(.*)"/; $code = $1; Log3 $name, 5, "$name - response_body: $response_body"; Log3 $name, 5, "$name - code: $code"; } else { Log3 $name, 1, "$name An error happened: $err"; } $param = { url => 'https://iam.viessmann.com/idp/v1/token', hash => $hash, header => "Content-Type: application/x-www-form-urlencoded;charset=utf-8", data => "client_id=$client_id&client_secret=$client_secret&code=$code&redirect_uri=$callback_uri&grant_type=authorization_code", sslargs => {SSL_verify_mode => 0}, method => "POST" }; ($err, $response_body) = HttpUtils_BlockingGet($param); if ($err eq "") { my $decode_json = eval{decode_json($response_body)}; if($@) { Log3 $name, 1, "$name - JSON error while request: $@"; return; } $access_token = $decode_json->{access_token}; Log3 $name, 5, "$name - access_token: $access_token"; } else { Log3 $name, 1, "$name: getAccessToken: An error happened: $err"; } $param = { url => "$apiURLBase$general", hash => $hash, header => "Authorization: Bearer $access_token", sslargs => {SSL_verify_mode => 0} }; ($err, $response_body) = HttpUtils_BlockingGet($param); if ($err eq "") { Log3 $name, 5, "$name - action (installation and gw): $response_body"; my $decode_json = eval{decode_json($response_body)}; if($@) { Log3 $name, 1, "$name - JSON error while request: $@"; return; } $installation = $decode_json->{entities}[0]->{properties}->{id}; $gw = $decode_json->{entities}[0]->{entities}[0]->{properties}->{serial}; Log3 $name, 4, "$name: installation: $installation :: gw: $gw" } else { Log3 $name, 1, "$name: An error happened: $err"; } return undef; } ################################################### # checks and stores obfuscated keys like passwords # based on / copied from FRITZBOX_storePassword sub vitoconnect_StoreKeyValue($$$) { my ($hash, $kName, $value) = @_; my $index = $hash->{TYPE}."_".$hash->{NAME}."_".$kName; my $key = getUniqueId().$index; my $enc = ""; if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (split //, $value) { my $encode=chop($key); $enc.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } my $err = setKeyValue($index, $enc); return "error while saving the value - $err" if(defined($err)); return undef; } ##################################################### # reads obfuscated value sub vitoconnect_ReadKeyValue($$) { my ($hash, $kName) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_".$kName; my $key = getUniqueId().$index; my ($value, $err); Log3 $name, 5, "$name: ReadKeyValue tries to read value for $kName from file"; ($err, $value) = getKeyValue($index); if ( defined($err) ) { Log3 $name, 1, "$name: ReadKeyValue is unable to read value from file: $err"; return undef; } if ( defined($value) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } my $dec = ''; for my $char (map { pack('C', hex($_)) } ($value =~ /(..)/g)) { my $decode=chop($key); $dec.=chr(ord($char)^ord($decode)); $key=$decode.$key; } return $dec; } else { Log3 $name, 1, "$name: ReadKeyValue could not find key $kName in file"; return undef; } return; } 1; =pod =item device =item summary support for Vissmann API =item summary_DE Unterstützung für die Vissmann API =begin html

vitoconnect

    vitoconnect implements a device for Vissmann API Vitoconnect100. Based on investigation of thetrueavatar You need the user and password from the ViCare App account. For details see: FHEM Wiki (german)

    Define
      define <name> vitoconnect <user> <password> <interval>
      It is a good idea to use a fake password here an set the correct one later because it is readable in the detail view of the device

      Example:
      define vitoconnect vitoconnect user@mail.xx fakePassword 60
      set vitoconnect password correctPassword 60


    Set
    • update
      update readings immeadiatlely
    • clearReadings
      clear all readings immeadiatlely
    • password
      store password in key store
    • HK1-Heizkurve-Niveau shift
      not implemented
    • HK1-Heizkurve-Steigung slope
      not implemented
    • HeatingSchedule
      not implemented
    • HK1-Betriebsart standby,dhw,dhwAndHeating,forcedReduced,forcedNormal
      set HK1-Betriebsart to standby,dhw,dhwAndHeating,forcedReduced or forcedNormal
    • HeadingModeComfort activate,deactivate
      not implemented
    • TemperatureComfort targetTemperature
      not implemented
    • HeatingModeEco activate,deactivate
      not implemented
    • HeatingHolidaySchedule start end
      not implemented
    • HeatingHolidayUnschedule
      not implemented
    • HK1-Solltemperatur_normal targetTemperature
      sets the normale target temperature where targetTemperature is an integer between 3 and 37
    • HK1-Solltemperatur_reduziert targetTemperature
      sets the reduced target temperature where targetTemperature is an integer between 3 and 37
    • WW-einmaliges_Aufladen activate,deactivate
      activate or deactivate one time charge for hot water
    • WW-Zirkulationspumpe_Zeitplan schedule
      not implemented
    • WW-Zeitplan schedule
      not implemented
    • WW-Haupttemperatur targetTemperature
      targetTemperature is an integer between 10 and 60
      not implemented
    • WW-Solltemperatur targetTemperature
      targetTemperature is an integer between 10 and 60
      not implemented

    Get
      nothing to get here

    Attributes
      attr <name> <attribute> <value>

      See commandref#attr for more info about the attr command.

      Attributes:
      • disable:
        stop communication with Vissmann server
      • verbose:
        set the verbosity level
      • vitoconnect_raw_readings:
        create readings with plain JSON names like 'heating.circuits.0.heating.curve.slope' instead of german identifiers
      • vitoconnect_actions_active:
        create readings for actions e.g. 'heating.circuits.0.heating.curve.setCurve'
    Readings

    vitoconnect sets one reading for every value delivered by the API (depends on the type and the settings of your heater and the version of the API!). Already known values will be mapped to clear names. Unknown values will added with their JSON path (e.g. "heating.burner.modulation.value"). Please report new readings to the module maintainer. A description of the known reading could be found here (german)
=end html =cut =cut