Buderus KM200 api - MichelDeLigne -  29.04.2019
 
 
Hello 
 
Here is a script I made that allows you to interface a LM5 with a Logamatic KM200 Buderus gateway. 
Logamatic web KM50, KM100, KM300 modules, and Junkers/Bosch MB LANi should also be supported in theory. 
  
This script allows you to read any endpoint from the REST interface of the gateway, and assign the values to a KNX group. 
It is also possible to set some of the values, ie. change from "auto" to "day" or "night" mode. 
Included in an event script, and with a few customizations at the end, you could control the central heating from a few KNX address groups. 
 
For all intent and purposes, this script gives you the same capabilities as the KNX10 module from Bosch, that is available almost nowhere, and is only in German. 
 
Because I do not read many endpoints in my case, I run the script in a cyclic mode on a 60s cycle.  
Accessing a lot of the endpoints would probably require some changes to the script to allow more, or less, polling on some of the endpoints (ie: 10min for the outdoor temperature, 15s for the mode, ...).  
This would reduce the load on the gateway and the network. 
 
Two scripts are required: A custom aes.lua library installed in the user library, and the km200.lua 
 
The aes.lua is different than the one published in another thread on the forum because this one supports no-padding and can decode strings encrypted in other tools, which the original library cannot do. 
 
The list of all endpoints that exist in my system (and some more) is included in the km200.lua script.  
Your system may have more or less depending on the hardware installed. 
 
Of course, if you decide to use the scripts, you do it at your own risk.  
I am not responsible if something goes wrong. 
 
That said, my house is still standing.     
 
Enjoy. 
Michel. 
 
KM200.lua: 
Code: -- 
-- Control of Buderus boilers using a Logamatic web KM200 module. 
-- Logamatic web KM50, KM100, KM300 modules, and Junkers/Bosch MB LANi, should also be supported. 
--  
-- Reading and writing from/to the gateway is supported. 
-- 
-- v1.0.0 - 2019-04-28 
-- Michel De Ligne 
-- 
 
encdec = require('encdec') 
aes = require('user.aes') 
json = require('json') 
ltn12 = require('ltn12') 
http = require('socket.http') 
 
-- 
-- !!! The three following parameters need to be filled in properly !!! 
-- 
-- IP address or DNS name of the Buderus KM200. 
local km200_gateway_host = "TK-850XXXNET.home" -- or "192.168.xxx.xxx" 
-- Gateway password - found on the sticker on the KM200. 
-- Remove the "-", and put everything in one word. 
local km200_gateway_password = "gateway_password" -- ie: fjbvJH5IjYndKdhf 
 
-- Password defined in the app. 
local km200_private_password = "app_password" -- ie: qweYTR!rtyEWQ 
 
-- 
-- Code 
-- 
 
-- MD5 salt used for the AES key generation (do not change!) 
local km200_crypt_md5_salt = string.char( 
        0x86, 0x78, 0x45, 0xe9, 0x7c, 0x4e, 0x29, 0xdc, 
        0xe5, 0x22, 0xb9, 0xa7, 0xd3, 0xa3, 0xe0, 0x7b, 
        0x15, 0x2b, 0xff, 0xad, 0xdd, 0xbe, 0xd7, 0xf5, 
        0xff, 0xd8, 0x42, 0xe9, 0x89, 0x5a, 0xd1, 0xe4 
); 
 
-- First half of the key: MD5 of Gateway password followed by km200_crypt_md5_salt 
local key_1 = encdec.md5(km200_gateway_password .. km200_crypt_md5_salt, true) 
-- Second part of the key - private: MD5 from salt followed by private_password 
local key_2_private = encdec.md5(km200_crypt_md5_salt .. km200_private_password, true) 
 
km200_crypt_key_private = key_1 .. key_2_private 
 
-- PKCS7 pad/unpad 
function pad(data, blocksize, optional) 
    blocksize = blocksize or 16 
    if type(blocksize) ~= "number" then 
        return nil, "invalid block size data type" 
    end 
    if blocksize < 1 or blocksize > 256 then 
        return nil, "invalid block size" 
    end 
    local ps = blocksize - #data % blocksize 
    if optional and ps == blocksize then return data end 
    return data .. string.rep(string.char(ps), ps) 
end 
 
-- PKCS7 unpad 
function unpad(data, blocksize) 
    blocksize = blocksize or 16 
    if type(blocksize) ~= "number" then 
        return nil, "invalid block size data type" 
    end 
    if blocksize < 1 or blocksize > 256 then 
        return nil, "invalid block size" 
    end 
    local len = #data 
    if len % blocksize ~= 0 then 
        return nil, "data length is not a multiple of the block size" 
    end 
    local chr = string.sub(data, -1) 
    local rem = string.byte(chr) 
    if rem > 0 and rem <= blocksize then 
        local chk = string.sub(data, -rem) 
        if chk == string.rep(chr, rem) then 
            return string.sub(data, 1, len - rem) 
        end 
    end 
    return data 
end 
 
function km200_Encrypt(encryptData) 
    -- add PKCS #7 padding 
    encryptData = pad(encryptData) 
 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0) 
    return encdec.base64enc(aes_256_ecb:encrypt(encryptData)) 
    end 
 
function km200_Decrypt(decryptData) 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0) 
    local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits 
 
    -- Search the position of the last "}", and remove all the remaining padding after that (I am lazy). 
    local endposition = string.find(res, "}", -0) 
    res = string.sub(res, 0, endposition) 
    return res 
end 
 
function dorequest(url) 
    local tbl = {} 
    local res, err = http.request({ 
        url = url, 
        headers = { 
            ['Accept'] = 'application/json', 
            ['User-Agent'] = 'TeleHeater/2.2.3' 
        }, 
        sink = ltn12.sink.table(tbl) 
    }) 
 
    if res then 
        return table.concat(tbl) 
    else 
        log('http error: ' .. err) 
        return nil 
    end 
end 
 
function km200_GetData(REST_URL) 
    local res = dorequest('http://' .. km200_gateway_host .. REST_URL) 
 
    if res then 
        return km200_Decrypt(res) 
    else 
        return nil 
    end 
end 
 
function doput(url, reqbody) 
    local tbl = {} 
    local res, err = http.request({ 
        url = url, 
        method = 'POST', 
        source = ltn12.source.string(reqbody), 
        headers = { 
            ['User-Agent'] = 'TeleHeater/2.2.3', 
            ['Content-Type'] = 'application/json', 
            ['Content-Length'] = string.len(reqbody) 
        }, 
        sink = ltn12.sink.table(tbl) 
    }) 
    if res then 
        return table.concat(tbl) 
    else 
        log('http error: ' .. err) 
        return nil 
    end 
end 
 
function km200_SetData(REST_URL, Value) 
    local content = json.encode({ value = Value }) 
    local res = doput('http://' .. km200_gateway_host .. REST_URL, km200_Encrypt(content)) 
 
    if res then 
        return km200_Decrypt(res) 
    else 
        return nil 
    end 
end 
 
-- List of all the REST endpoints available. 
-- The endpoints available depends on the system configuration (ie: solar vs no solar, type of regulation, ...) 
 
--[[ 
/dhwCircuits/dhw1 
/dhwCircuits/dhw1/actualTemp 
/dhwCircuits/dhw1/charge 
/dhwCircuits/dhw1/chargeDuration 
/dhwCircuits/dhw1/cpStartph 
/dhwCircuits/dhw1/currentSetpoint 
/dhwCircuits/dhw1/operationMode 
/dhwCircuits/dhw1/singleChargeSetpoint 
/dhwCircuits/dhw1/status 
/dhwCircuits/dhw1/switchPrograms 
/dhwCircuits/dhw1/tdMode 
/dhwCircuits/dhw1/tdsetPoint 
/dhwCircuits/dhw1/temperatureLevels 
/dhwCircuits/dhw1/temperatureLevels/high 
/dhwCircuits/dhw1/temperatureLevels/off 
/dhwCircuits/dhw1/waterFlow 
/dhwCircuits/dhw1/workingTime 
/gateway/boschSHPassword 
/gateway/DateTime 
/gateway/firmware 
/gateway/haiPassword 
/gateway/instAccess 
/gateway/instPassword 
/gateway/instWriteAccess 
/gateway/knxPassword 
/gateway/portalPassword 
/gateway/update 
/gateway/userpassword 
/gateway/uuid 
/gateway/version 
/gateway/versionFirmware 
/gateway/versionHardware 
/heatingCircuits 
/heatingCircuits/hc1 
/heatingCircuits/hc1/activeSwitchProgram 
/heatingCircuits/hc1/actualSupplyTemperature 
/heatingCircuits/hc1/controlType 
/heatingCircuits/hc1/currentOpModeInfo 
/heatingCircuits/hc1/currentRoomSetpoint 
/heatingCircuits/hc1/designTemp 
/heatingCircuits/hc1/fastHeatupFactor 
/heatingCircuits/hc1/heatCurveMax 
/heatingCircuits/hc1/heatCurveMin 
/heatingCircuits/hc1/manualRoomSetpoint 
/heatingCircuits/hc1/nextSetpoint 
/heatingCircuits/hc1/operationMode 
/heatingCircuits/hc1/pumpModulation 
/heatingCircuits/hc1/roomInfluence 
/heatingCircuits/hc1/roomtemperature 
/heatingCircuits/hc1/roomTempOffset 
/heatingCircuits/hc1/setpointOptimization 
/heatingCircuits/hc1/solarInfluence 
/heatingCircuits/hc1/status 
/heatingCircuits/hc1/suWiSwitchMode 
/heatingCircuits/hc1/suWiThreshold 
/heatingCircuits/hc1/switchPrograms 
/heatingCircuits/hc1/switchPrograms/A 
/heatingCircuits/hc1/switchPrograms/B 
/heatingCircuits/hc1/temperatureLevels 
/heatingCircuits/hc1/temperatureLevels/comfort2 
/heatingCircuits/hc1/temperatureLevels/eco 
/heatingCircuits/hc1/temperatureRoomSetpoint 
/heatingCircuits/hc1/temporaryRoomSetpoint 
/heatingCircuits/hc1/timeToNextSetpoint 
/heatSources 
/heatSources/actualCHPower 
/heatSources/actualDHWPower 
/heatSources/actualModulation 
/heatSources/actualPower 
/heatSources/actualSupplyTemperature 
/heatSources/applianceSupplyTemperature 
/heatSources/burnerModulationSetpoint 
/heatSources/burnerPowerSetpoint 
/heatSources/ChimneySweeper 
/heatSources/CHpumpModulation 
/heatSources/flameCurrent 
/heatSources/flameStatus 
/heatSources/gasAirPressure 
/heatSources/hs1 
/heatSources/hs1/actualCHPower 
/heatSources/hs1/actualDHWPower 
/heatSources/hs1/actualModulation 
/heatSources/hs1/actualPower 
/heatSources/hs1/CHpumpModulation 
/heatSources/hs1/energyReservoir 
/heatSources/hs1/flameStatus 
/heatSources/hs1/fuel 
/heatSources/hs1/fuel/caloricValue 
/heatSources/hs1/fuel/density 
/heatSources/hs1/fuelConsmptCorrFactor 
/heatSources/hs1/info 
/heatSources/hs1/nominalCHPower 
/heatSources/hs1/nominalDHWPower 
/heatSources/hs1/nominalFuelConsumption 
/heatSources/hs1/numberOfStarts 
/heatSources/hs1/reservoirAlert 
/heatSources/hs1/supplyTemperatureSetpoint 
/heatSources/hs1/type 
/heatSources/info 
/heatSources/nominalCHPower 
/heatSources/nominalDHWPower 
/heatSources/numberOfStarts 
/heatSources/powerSetpoint 
/heatSources/returnTemperature 
/heatSources/supplyTemperatureSetpoint 
/heatSources/systemPressure 
/heatSources/workingTime 
/heatSources/workingTime/centralHeating 
/heatSources/workingTime/secondBurner 
/heatSources/workingTime/totalSystem 
/notifications 
/recordings 
/recordings/heatingCircuits 
/recordings/heatingCircuits/hc1 
/recordings/heatingCircuits/hc1/roomtemperature 
/recordings/heatSources 
/recordings/heatSources/actualCHPower 
/recordings/heatSources/actualDHWPower 
/recordings/heatSources/actualPower 
/recordings/heatSources/hs1 
/recordings/heatSources/hs1/actualPower 
/recordings/system 
/recordings/system/heatSources 
/recordings/system/heatSources/hs1 
/recordings/system/heatSources/hs1/actualPower 
/recordings/system/sensors 
/recordings/system/sensors/temperatures 
/recordings/system/sensors/temperatures/outdoor_t1 
/solarCircuits 
/solarCircuits/sc1/collectorTemperature 
/solarCircuits/sc1/pumpModulation 
/solarCircuits/sc1/solarYield 
/solarCircuits/sc1/status 
/system 
/system/appliance 
/system/appliance/actualPower 
/system/appliance/actualSupplyTemperature 
/system/appliance/ChimneySweeper 
/system/appliance/CHpumpModulation 
/system/appliance/flameCurrent 
/system/appliance/gasAirPressure 
/system/appliance/nominalBurnerLoad 
/system/appliance/numberOfStarts 
/system/appliance/powerSetpoint 
/system/appliance/systemPressure 
/system/appliance/workingTime 
/system/appliance/workingTime/centralHeating 
/system/appliance/workingTime/secondBurner 
/system/appliance/workingTime/totalSystem 
/system/brand 
/system/bus 
/system/healthStatus 
/system/heatSources 
/system/heatSources/hs1 
/system/heatSources/hs1/actualModulation 
/system/heatSources/hs1/actualPower 
/system/heatSources/hs1/energyReservoir 
/system/heatSources/hs1/fuel 
/system/heatSources/hs1/fuel/caloricValue 
/system/heatSources/hs1/fuel/density 
/system/heatSources/hs1/fuelConsmptCorrFactor 
/system/heatSources/hs1/nominalFuelConsumption 
/system/heatSources/hs1/reservoirAlert 
/system/holidayModes 
/system/holidayModes/hm1 
/system/holidayModes/hm1/assignedTo 
/system/holidayModes/hm1/delete 
/system/holidayModes/hm1/dhwMode 
/system/holidayModes/hm1/hcMode 
/system/holidayModes/hm1/startStop 
/system/holidayModes/hm2 
/system/holidayModes/hm2/assignedTo 
/system/holidayModes/hm2/delete 
/system/holidayModes/hm2/dhwMode 
/system/holidayModes/hm2/hcMode 
/system/holidayModes/hm2/startStop 
/system/holidayModes/hm3 
/system/holidayModes/hm3/assignedTo 
/system/holidayModes/hm3/delete 
/system/holidayModes/hm3/dhwMode 
/system/holidayModes/hm3/hcMode 
/system/holidayModes/hm3/startStop 
/system/holidayModes/hm4 
/system/holidayModes/hm4/assignedTo 
/system/holidayModes/hm4/delete 
/system/holidayModes/hm4/dhwMode 
/system/holidayModes/hm4/hcMode 
/system/holidayModes/hm4/startStop 
/system/holidayModes/hm5 
/system/holidayModes/hm5/assignedTo 
/system/holidayModes/hm5/delete 
/system/holidayModes/hm5/dhwMode 
/system/holidayModes/hm5/hcMode 
/system/holidayModes/hm5/startStop 
/system/info 
/system/minOutdoorTemp 
/system/sensors 
/system/sensors/temperatures 
/system/sensors/temperatures/chimney 
/system/sensors/temperatures/hotWater_t1 
/system/sensors/temperatures/hotWater_t2 
/system/sensors/temperatures/outdoor_t1 
/system/sensors/temperatures/return 
/system/sensors/temperatures/supply_t1 
/system/sensors/temperatures/supply_t1_setpoint 
/system/sensors/temperatures/switch 
/system/systemType 
]] 
 
-- 
-- Mapping of some endpoints to KNX group addresses 
-- 
 
-- {"id":"/heatSources/flameStatus","type":"stringValue","writeable":0,"recordable":1,"value":"off","allowedValues":["on","off"]} 
jsondata = json.decode(km200_GetData("/heatSources/flameStatus")) 
if (jsondata.value == 'on') then 
    grp.write('heatSources_flameStatus', true) 
else 
    grp.write('heatSources_flameStatus', false) 
end 
 
-- Afficher la performance actuelle Générateur de chaleur (5.001) 
-- {"id":"/heatSources/actualCHPower","type":"floatValue","writeable":0,"recordable":1,"value":0.0,"unitOfMeasure":"%"} 
jsondata = json.decode(km200_GetData("/heatSources/actualCHPower")) 
grp.write('heatSources_actualCHPower', jsondata.value) 
 
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/roomtemperature")) 
grp.write('heatingCircuits_hc1_roomtemperature', jsondata.value) 
 
-- {"id":"/system/sensors/temperatures/outdoor_t1","type":"floatValue","writeable":0,"recordable":1,"value":14.0,"unitOfMeasure":"C"} 
jsondata = json.decode(km200_GetData("/system/sensors/temperatures/outdoor_t1")) 
grp.write('temperatures_outdoor_t1', jsondata.value) 
 
-- {"id":"/heatSources/actualSupplyTemperature","type":"floatValue","writeable":0,"recordable":0,"value":21.7,"unitOfMeasure":"C"} 
jsondata = json.decode(km200_GetData("/heatSources/actualSupplyTemperature")) 
grp.write('heatSources_actualSupplyTemperature', jsondata.value) 
 
-- {"id":"/system/healthStatus","type":"stringValue","writeable":0,"recordable":0,"value":"ok"} 
jsondata = json.decode(km200_GetData("/system/healthStatus")) 
if (jsondata.value == 'ok') then 
    grp.write('system_healthStatus', false) 
else 
    grp.write('system_healthStatus', true) 
end 
 
-- {"id":"/heatingCircuits/hc1/operationMode","type":"stringValue","writeable":1,"recordable":1,"value":"auto","allowedValues":["night","day","auto"]} 
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/operationMode")) 
if (jsondata.value == 'auto') then 
    grp.write('heatingCircuits_operationMode_Auto', true) 
else 
    grp.write('heatingCircuits_operationMode_Auto', false) 
    if (jsondata.value == 'night') then 
        grp.write('heatingCircuits_operationMode_NightDay', false) 
    else 
        grp.write('heatingCircuits_operationMode_NightDay', true) 
    end 
end 
 
-- {"id":"/heatingCircuits/hc1/currentRoomSetpoint","type":"floatValue","writeable":0,"recordable":1,"value":18.0,"unitOfMeasure":"C","minValue":0.0,"maxValue":30.0} 
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/currentRoomSetpoint")) 
grp.write('heatingCircuits_hc1_currentRoomSetpoint', jsondata.value) 
 
-- {"id":"/heatingCircuits/hc1/pumpModulation","type":"floatValue","writeable":0,"recordable":1,"value":100.0,"unitOfMeasure":"%","minValue":0.0,"maxValue":100.0} 
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/pumpModulation")) 
if (jsondata.value == '0.0') then 
    grp.write('heatingCircuits_hc1_pumpModulation', false) 
else 
    grp.write('heatingCircuits_hc1_pumpModulation', true) 
end 
 
-- {"id":"/dhwCircuits/dhw1/actualTemp","type":"floatValue","writeable":0,"recordable":1,"value":57.8,"unitOfMeasure":"C"} 
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/actualTemp")) 
grp.write('dhwCircuits_dhw1_actualTemp', jsondata.value) 
 
 
-- {"id":"/dhwCircuits/dhw1/currentSetpoint","type":"floatValue","writeable":0,"recordable":1,"value":10.0,"unitOfMeasure":"C","minValue":0.0,"maxValue":80.0} 
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/currentSetpoint")) 
grp.write('dhwCircuits_dhw1_currentSetpoint', jsondata.value) 
 
-- {"id":"/dhwCircuits/dhw1/operationMode","type":"stringValue","writeable":1,"recordable":0,"value":"auto","allowedValues":["auto","on","off"]} 
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/operationMode")) 
if (jsondata.value == 'auto') then 
    grp.write('dhwCircuits_dhw1_operationMode_Auto', true) 
else 
    grp.write('dhwCircuits_dhw1_operationMode_Auto', false) 
    if (jsondata.value == 'on') then 
        grp.write('dhwCircuits_dhw1_operationManualOnOff', true) 
    else 
        grp.write('dhwCircuits_dhw1_operationManualOnOff', false) 
    end 
end 
 
-- Change the operation mode to auto mode (it needs to be one of the allowedValues): 
-- km200_SetData("/heatingCircuits/hc1/operationMode", "auto")
  
aes.lua 
Code: -- Copyright (C) by Yichun Zhang (agentzh) 
 
 
--local asn1 = require "resty.asn1" 
local ffi = require "ffi" 
local ffi_new = ffi.new 
local ffi_gc = ffi.gc 
local ffi_str = ffi.string 
local ffi_copy = ffi.copy 
local C = ffi.C 
local setmetatable = setmetatable 
--local error = error 
local type = type 
 
 
local _M = { _VERSION = '0.09' } 
 
local mt = { __index = _M } 
 
local lib = '/usr/lib/libcrypto.so' 
if not io.exists(lib) then 
     lib = '/usr/lib/libcrypto.so.1.0.0' 
end 
ffi.load(lib, true) 
 
ffi.cdef[[ 
typedef struct engine_st ENGINE; 
 
typedef struct evp_cipher_st EVP_CIPHER; 
typedef struct evp_cipher_ctx_st 
{ 
const EVP_CIPHER *cipher; 
ENGINE *engine; 
int encrypt; 
int buf_len; 
 
unsigned char  oiv[16]; 
unsigned char  iv[16]; 
unsigned char buf[32]; 
int num; 
 
void *app_data; 
int key_len; 
unsigned long flags; 
void *cipher_data; 
int final_used; 
int block_mask; 
unsigned char final[32]; 
} EVP_CIPHER_CTX; 
 
typedef struct env_md_ctx_st EVP_MD_CTX; 
typedef struct env_md_st EVP_MD; 
 
const EVP_MD *EVP_md5(void); 
const EVP_MD *EVP_sha(void); 
const EVP_MD *EVP_sha1(void); 
const EVP_MD *EVP_sha224(void); 
const EVP_MD *EVP_sha256(void); 
const EVP_MD *EVP_sha384(void); 
const EVP_MD *EVP_sha512(void); 
 
const EVP_CIPHER *EVP_aes_128_ecb(void); 
const EVP_CIPHER *EVP_aes_128_cbc(void); 
const EVP_CIPHER *EVP_aes_128_cfb1(void); 
const EVP_CIPHER *EVP_aes_128_cfb8(void); 
const EVP_CIPHER *EVP_aes_128_cfb128(void); 
const EVP_CIPHER *EVP_aes_128_ofb(void); 
const EVP_CIPHER *EVP_aes_128_ctr(void); 
const EVP_CIPHER *EVP_aes_192_ecb(void); 
const EVP_CIPHER *EVP_aes_192_cbc(void); 
const EVP_CIPHER *EVP_aes_192_cfb1(void); 
const EVP_CIPHER *EVP_aes_192_cfb8(void); 
const EVP_CIPHER *EVP_aes_192_cfb128(void); 
const EVP_CIPHER *EVP_aes_192_ofb(void); 
const EVP_CIPHER *EVP_aes_192_ctr(void); 
const EVP_CIPHER *EVP_aes_256_ecb(void); 
const EVP_CIPHER *EVP_aes_256_cbc(void); 
const EVP_CIPHER *EVP_aes_256_cfb1(void); 
const EVP_CIPHER *EVP_aes_256_cfb8(void); 
const EVP_CIPHER *EVP_aes_256_cfb128(void); 
const EVP_CIPHER *EVP_aes_256_ofb(void); 
 
void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a); 
int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *a); 
 
int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *ctx, int padding); 
 
int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher, 
        ENGINE *impl, unsigned char *key, const unsigned char *iv); 
 
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, 
        const unsigned char *in, int inl); 
 
int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); 
 
int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher, 
        ENGINE *impl, unsigned char *key, const unsigned char *iv); 
 
int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, 
        const unsigned char *in, int inl); 
 
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl); 
 
int EVP_BytesToKey(const EVP_CIPHER *type,const EVP_MD *md, 
        const unsigned char *salt, const unsigned char *data, int datal, 
        int count, unsigned char *key,unsigned char *iv); 
]] 
 
local ctx_ptr_type = ffi.typeof("EVP_CIPHER_CTX[1]") 
 
local hash 
hash = { 
    md5 = C.EVP_md5(), 
    sha1 = C.EVP_sha1(), 
    sha224 = C.EVP_sha224(), 
    sha256 = C.EVP_sha256(), 
    sha384 = C.EVP_sha384(), 
    sha512 = C.EVP_sha512() 
} 
_M.hash = hash 
 
local cipher 
cipher = function (size, _cipher) 
    local _size = size or 128 
    local _cipher = _cipher or "cbc" 
    local func = "EVP_aes_" .. _size .. "_" .. _cipher 
    if C[func] then 
        return { size=_size, cipher=_cipher, method=C[func]()} 
    else 
        return nil 
    end 
end 
_M.cipher = cipher 
 
function _M.new(self, key, salt, _cipher, _hash, hash_rounds, padding) 
    local encrypt_ctx = ffi_new(ctx_ptr_type) 
    local decrypt_ctx = ffi_new(ctx_ptr_type) 
    local _cipher = _cipher or cipher() 
    local _hash = _hash or hash.md5 
    local hash_rounds = hash_rounds or 1 
    local padding = padding or 1 
    local _cipherLength = _cipher.size/8 
    local gen_key = ffi_new("unsigned char[?]",_cipherLength) 
    local gen_iv = ffi_new("unsigned char[?]",_cipherLength) 
 
    if type(_hash) == "table" then 
        if not _hash.iv or #_hash.iv ~= 16 then 
          return nil, "bad iv" 
        end 
 
        if _hash.method then 
            local tmp_key = _hash.method(key) 
 
            if #tmp_key ~= _cipherLength then 
                return nil, "bad key length" 
            end 
 
            ffi_copy(gen_key, tmp_key, _cipherLength) 
 
        elseif #key ~= _cipherLength then 
            return nil, "bad key length" 
 
        else 
            ffi_copy(gen_key, key, _cipherLength) 
        end 
 
        ffi_copy(gen_iv, _hash.iv, 16) 
 
    else 
        if C.EVP_BytesToKey(_cipher.method, _hash, salt, key, #key, 
                            hash_rounds, gen_key, gen_iv) 
            ~= _cipherLength 
        then 
            return nil 
        end 
    end 
 
    C.EVP_CIPHER_CTX_init(encrypt_ctx) 
    C.EVP_CIPHER_CTX_init(decrypt_ctx) 
 
    if C.EVP_EncryptInit_ex(encrypt_ctx, _cipher.method, nil, 
      gen_key, gen_iv) == 0 or 
      C.EVP_DecryptInit_ex(decrypt_ctx, _cipher.method, nil, 
      gen_key, gen_iv) == 0 then 
        return nil 
    end 
 
    if C.EVP_CIPHER_CTX_set_padding(encrypt_ctx, padding) == 0 then 
        return nil 
    end 
 
    if C.EVP_CIPHER_CTX_set_padding(decrypt_ctx, padding) == 0 then 
        return nil 
    end 
 
    ffi_gc(encrypt_ctx, C.EVP_CIPHER_CTX_cleanup) 
    ffi_gc(decrypt_ctx, C.EVP_CIPHER_CTX_cleanup) 
 
    return setmetatable({ 
      _encrypt_ctx = encrypt_ctx, 
      _decrypt_ctx = decrypt_ctx 
      }, mt) 
end 
 
 
function _M.encrypt(self, s) 
    local s_len = #s 
    local max_len = s_len + 16 
    local buf = ffi_new("unsigned char[?]", max_len) 
    local out_len = ffi_new("int[1]") 
    local tmp_len = ffi_new("int[1]") 
    local ctx = self._encrypt_ctx 
 
    if C.EVP_EncryptInit_ex(ctx, nil, nil, nil, nil) == 0 then 
        return nil 
    end 
 
    if C.EVP_EncryptUpdate(ctx, buf, out_len, s, s_len) == 0 then 
        return nil 
    end 
 
    if C.EVP_EncryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then 
        return nil 
    end 
 
    return ffi_str(buf, out_len[0] + tmp_len[0]) 
end 
 
 
function _M.decrypt(self, s) 
    local s_len = #s 
    local buf = ffi_new("unsigned char[?]", s_len) 
    local out_len = ffi_new("int[1]") 
    local tmp_len = ffi_new("int[1]") 
    local ctx = self._decrypt_ctx 
 
    if C.EVP_DecryptInit_ex(ctx, nil, nil, nil, nil) == 0 then 
      return nil 
    end 
 
    if C.EVP_DecryptUpdate(ctx, buf, out_len, s, s_len) == 0 then 
      return nil 
    end 
 
    if C.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then 
        return nil 
    end 
 
    return ffi_str(buf, out_len[0] + tmp_len[0]) 
end 
 
 
return _M
  
 
 
 
RE: Buderus KM200 api - sds -  30.10.2019
 
 
Hello Michel 
 
Is it possible that line 101: 
 
Code: -- Search the position of the last "}", and remove all the remaining padding after that (I am lazy). 
   local endposition = string.find(res, "}", -0)
  
needs to be -1 instead of -0. With your -0 I have faulty results, such as with this REST response 
 
Code: "id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8},{"short":3276.7}]}
  
 
 
 
RE: Buderus KM200 api - MichelDeLigne -  31.10.2019
 
 
Hi, 
 
Could be that there is an error left somewhere as once in a while I see an error in the log. 
I don't think it is related to the line 101. REST sends a json results, that is supposed to be enclosed in {}. 
In your case, I would say the problem is probably with the first "{", that seems to be missing. 
 
Regards. 
Michel.
 
 
 
RE: Buderus KM200 api - sds -  01.11.2019
 
 
 (31.10.2019, 08:49)MichelDeLigne Wrote:  I don't think it is related to the line 101. REST sends a json results, that is supposed to be enclosed in {}. 
In your case, I would say the problem is probably with the first "{", that seems to be missing. That was a copy paste error from my side. Real JSON answer is: 
Code: {"id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8},{"short":3276.7}]}
  By the way, as in the code you are searching for } it doesn't have anything to do with the {.  
 
Let me rephrase the issue. 
The output that I got after the code: 
Code: local endposition = string.find(res, "}", -0) 
res = string.sub(res, 0, endposition) 
return res
  is 
Code: {"id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8}
  This is not a valid JSON response as the last } (and some more text) is missing. I assume the -0 in the string.find is not the correct value. 
 
Anyway, thanks a lot for the code   It works like a charm so I'm happy with it!
 
 
 
RE: Buderus KM200 api - m.j.sorokin -  28.07.2020
 
 
Good day! I'm trying a lice script with a Buderus KM200 V2 device. When I try to run the script, I get the following error: 
 
km200 28.07.2020 21:34:09 
User library aes:178: Symbol not found: EVP_CIPHER_CTX_init 
stack traceback: 
 [C]: in function '__index' 
 User library aes:178: in function 'new' 
 Resident script:97: in function 'km200_GetData' 
 
What could be the reason?
 
 
 
RE: Buderus KM200 api - MichelDeLigne -  28.07.2020
 
 
 (28.07.2020, 18:39)m.j.sorokin Wrote:  Good day! I'm trying a lice script with a Buderus KM200 V2 device. When I try to run the script, I get the following error: 
 
km200 28.07.2020 21:34:09 
User library aes:178: Symbol not found: EVP_CIPHER_CTX_init 
stack traceback: 
 [C]: in function '__index' 
 User library aes:178: in function 'new' 
 Resident script:97: in function 'km200_GetData' 
 
What could be the reason?  
Hello. 
Are you running by any chances on the latest software release of LM? 
I have the same problem since I updated. There is a problem with the aes library with the 2020 RC2 version.
 
 
 
RE: Buderus KM200 api - admin -  29.07.2020
 
 
This happened because new FW uses newer OpenSSL library which changed some of the function calls. I've attached an updated aes library see if it works for you.
 
 
 
RE: Buderus KM200 api - MichelDeLigne -  29.07.2020
 
 
Thanks for the updated  aes library. It does solve the problem.
 
 
 
RE: Buderus KM200 api - m.j.sorokin -  29.07.2020
 
 
Thanks, the new library solved the problem, some of the variables were updated, but I got the following error: 
 
 
km200 29.07.2020 16:32:36 
Resident script:397: Expected comma or array end but found T_END at character 156 
stack traceback: 
 [C]: in function 'decode' 
 
This may be due to the fact that on my device some of the functionality may be inactive?
 
 
 
RE: Buderus KM200 api - admin -  30.07.2020
 
 
Replace km200_Decrypt function with this. Original function removes all data after the first "}" while it should remove it after the last "}", this is why you get an error when there are multiple "}" in returned data. 
Code: function km200_Decrypt(decryptData) 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0) 
    local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits 
 
    -- Search the position of the last "}", and remove all the remaining padding after that 
    local endposition 
    while true do 
      local pos = res:find("}", endposition) 
      if pos then 
          endposition = pos + 1 
      else 
          break 
      end 
    end 
 
    if endposition then 
        res = res:sub(1, endposition - 1) 
    end 
    return res 
end
  
 
 
 
RE: Buderus KM200 api - m.j.sorokin -  30.07.2020
 
 
Thank you so much! A new feature allows parse all parameters, including all current temperatures!
 
 
 
RE: Buderus KM200 api - MichelDeLigne -  30.07.2020
 
 
 (30.07.2020, 06:20)admin Wrote:  Replace km200_Decrypt function with this. Original function removes all data after the first "}" while it should remove it after the last "}", this is why you get an error when there are multiple "}" in returned data. 
Code: function km200_Decrypt(decryptData) 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0) 
    local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits 
 
    -- Search the position of the last "}", and remove all the remaining padding after that 
    local endposition 
    while true do 
      local pos = res:find("}", endposition) 
      if pos then 
          endposition = pos + 1 
      else 
          break 
      end 
    end 
 
    if endposition then 
        res = res:sub(1, endposition - 1) 
    end 
    return res 
end
   
Thanks admin for the correction. 
 
Do you know why a find with a "-" does not work? 
in Lua, if I am not mistaken, a find with a "-" in the argument would start the search from the end and find the first occurence of the character. That should find the last "}". 
Obviously it does not work, so I am missing something here. 
 
Regards. 
Michel
 
 
 
RE: Buderus KM200 api - admin -  30.07.2020
 
 
This argument is for where to start the search from. A negative value means start from "string length - abs(argument)". The search is still performed from the position to the end of the string. There's no built-in function for reverse search.
 
 
 
RE: Buderus KM200 api - sds -  20.05.2024
 
 
Hi all 
 
Just upgraded from 20230607 to 20240426 version, with error message 
 
Code: User library aes:185: Symbol not found: EVP_CIPHER_CTX_block_size 
stack traceback: 
[C]: in function '__index' 
User library aes:185: in function 'new' 
User script:107: in function 'km200_GetData'
  
I updated aes.lua as suggested in https://forum.logicmachine.net/showthread.php?tid=2390&pid=34799#pid34799 
 
However, I stumble now to another error (while the Buderus REST API did not change) 
 
Reading the output of the error message thrown by aes decrypt function 
Code: local res, err2 = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits 
  alert (err2)
  
resulted in: "EVP_DecryptFinal_ex failed" 
 
Which is thrown by following code 
 
Code:     if C.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then 
        return nil, "EVP_DecryptFinal_ex failed" 
    end
  
Any suggestion? What info is needed to debug further?
 
 
 
RE: Buderus KM200 api - admin -  20.05.2024
 
 
Can you send raw HTTP response via PM (in base64 format before decryption)?
 
 
 
RE: Buderus KM200 api - sds -  20.05.2024
 
 
PM is sent
 
 
 
RE: Buderus KM200 api - admin -  21.05.2024
 
 
Try disabling padding in km200_Encrypt and km200_Decrypt: 
 
Code: function km200_Encrypt(encryptData) 
    -- add PKCS #7 padding 
    encryptData = pad(encryptData) 
 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0, false) -- no padding
  
Code: function km200_Decrypt(decryptData) 
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key 
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0, false) -- no padding
  
 
 
 
RE: Buderus KM200 api - sds -  21.05.2024
 
 
We have a winner! Much appreciated Admin
 
 
 
 |