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
|