Tedee Smartlock integration via rules and http calls

Hi there,

this is a configuration example to make a tedee smartlock accessible and usable in the Openhab system by using API calls in rules.
There is no local API support yet, hence online access is required.
A connected bridge is required to make use of the API

Disclaimer: I admit I tried to get this going in a binding, but my Java knowledge is quite limited and I am sitting in front of Eclipse without any idea on how to start.

First of all I created a few basic items:

String tedee_token
Number tedee_timer
Number tedee_batteryLevel "Schloss Batteriestand [%.0f %%]"
String tedee_isConnected "Status Schloss [%s]"
String tedee_state "Zustand Schloss [%s]"
Switch tedee_tokenfetcher "Holt Token" {expire="1s,state=OFF"}
String tedee_lockoperation "Schlossbedienung []"

I am accessing the api first to get a token from the API with my tedee username and password:

rule "Retrieve Token"
when
	Item tedee_tokenfetcher changed from OFF to ON
then
var String api = "https://tedee.b2clogin.com/tedee.onmicrosoft.com/B2C_1_SignIn_Ropc/oauth2/v2.0/token"
var String result = ""

var String username = "your username/email"
var String password = "your password"

var String content = "grant_type=password&username=" + username + "&password=" + password + "&client_id=02106b82-0524-4fd3-ac57-af774f340979&scope=openid 02106b82-0524-4fd3-ac57-af774f340979&response_type=token"

result = sendHttpPostRequest(api, "application/x-www-form-urlencoded", content, 1000)
val evaluation = transform("JSONPATH", "$.error", result)

if (evaluation != "access_denied") {
	val token = transform("JSONPATH", "$.access_token", result)
	val time = transform("JSONPATH", "$.expires_in", result)
	logInfo("tedee", " Updated Token for tedee: " + token)
	tedee_token.postUpdate(token)
	tedee_timer.postUpdate(time)
} else {
	logInfo("tedee","API threw error, restarting in 5 minutes")
	createTimer(now.plusMinutes(5), [|
		tedee_tokenfetcher.sendCommand(ON)
	])
}

if (tedee_token.state == null ) {
	 logInfo("tedee","API threw error, restarting in 5 minutes")
        createTimer(now.plusMinutes(5), [|
                tedee_tokenfetcher.sendCommand(ON)
        ])
}end

As each token is valid for 10800 seconds I set a timer which fetches a new token 5 minutes before the old one expires. The rule is working based on the API expires_in response, in case the time limit changes.

rule "Token update Timer"
when 
	Item tedee_timer received update
then

val int timeToUpdate = (tedee_timer.state as Number).intValue() - 300
logInfo("tedee", "Fetching new token in " + timeToUpdate + " seconds")
createTimer(now.plusSeconds(timeToUpdate), [|
	tedee_tokenfetcher.sendCommand(ON)
])
end


With the token, you are able to fetch all connected logs and get the status of respective locks from the API. I update the corresponding items where necessary and run it every 10 minutes. tedee advises not to sync status faster than every 10 seconds. Generally even a slower update would be feasible in my case, as I am merely interested in whether it is locked or not and battery stats.

The lock id changes each time the lock is deleted and re-added to your tedee account. The id can be retrieved by running a full sync against the api (https://api.tedee.com/api/v1.20/my/lock/sync) without a specific id.
I did not automate this, as I do not expect a lot of changes. I used the uri once during the first call and logged it.

rule "Retrieve Lock status and update items"
when
	Time cron "0 0/10 * * * ?"
then

var String lock_id = "your lock id"
var String api = "https://api.tedee.com/api/v1.20/my/lock/" + lock_id + "/sync"
var String tokenheader = "Bearer " + tedee_token.state.toString
var headers = newHashMap("accept" -> "application/json","Authorization" -> tokenheader)
var String result = ""

	result = sendHttpGetRequest(api,headers,1000) 
	if (result.equals("{}")) {
	tedee_tokenfetcher.sendCommand(ON)
}

	//in case of error, start new token fetch
val evaluation = transform("JSONPATH", "$.success", result)
if (evaluation == "false") {
	logInfo("tedee","API threw error, fetching token")
	tedee_tokenfetcher.sendCommand(ON)
} 

val isConnected = transform("JSONPATH", "$.result.isConnected", result) 
	val state_int = transform("JSONPATH", "$.result.lockProperties.state", result)
	val batteryLevel = Integer::parseInt(transform("JSONPATH", "$.result.lockProperties.batteryLevel", result))
	logInfo("tedee", "Schloss ID " + lock_id + " ist verbunden (" + isConnected + "), aktueller Status " + state_int + " und Batterie " + batteryLevel + "%")

//in case of error, start new token fetch
val evaluation = transform("JSONPATH", "$.error", result)
if (evaluation == "access_denied") {
	logInfo("tedee","API threw error, fetching token")
	tedee_tokenfetcher.sendCommand(ON)
} 

	//update Lockitems only if information has changed
	if (isConnected == "true" && tedee_isConnected != "Verbunden" ) {
		tedee_isConnected.postUpdate("Verbunden")
	} else {
		tedee_isConnected.postUpdate("Getrennt")
	}


	if (tedee_batteryLevel.state == NULL || batteryLevel != tedee_batteryLevel.state) {
		tedee_batteryLevel.postUpdate(batteryLevel)
	}
	
	if (tedee_state.state == NULL || state_int != tedee_state.state.toString) {
		tedee_state.postUpdate(state_int)
	}
	
	
		if (state_int == "2" || state_int == "6") {  // Updates visual representation of Lock in the Basic UI
			tedee_lockoperation.postUpdate(state_int)
		}
end

I am still using basic sitemap with the ios app and use following rule to initiate a lockoperation through HTTP Post.
Unfortunately the “soft lock” mode without pulling the trap is NOT working as long as trap pulling is already configured on the lock.

rule "tedee Bedienung"
when
	Item tedee_lockoperation received update
then

var String lock_id = "your lock id"
var String api = "https://api.tedee.com/api/v1.20/my/lock/" + lock_id + "/operation"

var String tokenheader = "Bearer " + tedee_token.state.toString
var headers = newHashMap("accept" -> "application/json","Authorization" -> tokenheader)

switch(tedee_lockoperation.state) {
	case "2": {
		api = api + "/unlock"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(2)
	}

	case "6": {
		api = api + "/lock"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(6)
	}

	case "99": {
		api = api + "/unlock?mode=3"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(2)
	}
}
end

In my Basic sitemap I used basic item integration

Text label="Schloss" {
            Text item=tedee_isConnected 
			Switch item=tedee_lockoperation mappings=[2="Oeffnen",6="Schliessen",99="Soft öffnen"]
            Text item=tedee_batteryLevel
            Text item=tedee_state label="Status Schloss [MAP(tedee_lockstates.map):%s]"
			Switch item=tedee_tokenfetcher
        }

Maybe this helps somebody with these locks. I like the small form factor compared to other options as well as the possibility to load via USB. It is not necessary to connect the bridge for basic use, but without it the lock is only exposing bluetooth.

Happy to receive any feedback on how to make the rules better or on how to get the soft opening (mode 3 ) working.

5 Likes

Thank you so much for this! I got it working with your code. I was almost considering moving to a Homey device because a plugin is already available there :smiley: I really love the Tedee lock so far and was eager to get the lock to close automatically as part of my ‘bedtime’ rule.

Some notes:

  • I think the JSONPath Transformation needed for this to work isn’t installed by default in OpenHab
  • For some reason I couldn’t use the APIs with the token from the RPOC flow in order to get the lock ID. It’s throwing a ‘not authenticated’. I used Postman and that did work
  • According to the Tedee documentation the ROPC Flow that you’ve used will be deprecated in Q4 2021. The suggested alternative of using Code Flow requires requesting a Client ID via an extensive form so hopefully this will be simplified in the future.

Did you get the token first from the API?
You have to invoke the first rule once, so it retrieves the token into a string item for the next 10800 seconds.

Indeed they will change access somehow. Maybe I figure out how to program a binding until then.

They also announced a local api, so eventually the rules can simply be adjusted to access the bridge locally without token stuff.

I meant on Tedee’s page with APIs it doesn’t accept the token as authorization.

Does your code continue to work over a longer period for you? For me the refresh of the token isn’t working. It’s throwing this error:

2021-08-30 08:50:01.227 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'tedee-3' failed: For input string: "{
  "success": false,
  "errorMessages": [
    "You are unauthorized."
  ],
  "statusCode": 401
}" in tedee

The response from the API doesn’t contain ‘error’ so now I’m checking whether ‘success’ is not true. I’m fairly new to this so perhaps I’m misunderstanding.

{"result":{"id":123456,"isConnected":true,"lockProperties":{"state":2,"isCharging":false,"batteryLevel":63,"stateChangeResult":0,"lastStateChangedDate":"2021-08-30T06:36:28.779"}},"success":true,"errorMessages":[],"statusCode":200}

Hi Aernout,
yes it is working for me over longer periods. I started the system yesterday and just checked the logs to confirm that last token updates was 20 minutes ago.

When you enter your token on the API page you have to put „Bearer „ first into the box and then your token.
if you put only the token it will throw an error.

i just tried it with a wrong password and it immediately went into error mode. So I suppose the error message is different then for the initial check. the check in this rule is mainly there to avoid problems without having a token. once the token is there it should run continously due to the timer rule. Seems I did not take into account that there may be a wrong token or different message. one could of course add another check based on errorMessage (or success like you did)

if the rule fails due to non authorization I would check whether there is an actual token present in the item tedee_token. during my tests it showed that it was best to fetch a token once on restart or when rules are reloaded to restart the timers. I do this in a system startup rule.

I just asked them yesterday: sadly it got postponed in backlog to 2022. I’m also waiting for over a year now for the local API in the bridge


PS: your content-type “application/json-patch+json” for lock/unlock operation doesn’t make sense regards the API documentation.

thanks for clarifying how to use the API page of Tedee.

The issue that I was having was indeed caused by reloading of the rules causing the timer to lose context and fail.

Actually I copied the content type from the api documentation page for unlocking.
it is mentioned here: https://tedee-tedee-api-doc.readthedocs-hosted.com/en/latest/endpoints/lock/unlock.html

sad to hear about the backlog. i honestly hope that they continue the line and bring some of the features they suggested (keypad for example)

haha, yea interesting. have a look here :slight_smile: Swagger UI
so different on swagger ui. But I’ve checked through the API versions and I believe that your link isn’t up2date compared to swagger which might be the reason for it.

yes
 I just hope that their cloud will be safe, highly available and not discontinued one day before the local API will be available.

you are right. swagger ist now on 1.21 while documentation was still referencing 1.2.
Bute even in documentation I now found that under different places it is posted with and without this ContentType. It does work though regardless of the additional/wrong ContentType.

1 Like

Because from v1.20-v1.21 they moved everything out of the http body to parameters in URL. in <= v1.19 it is a JSON string you send in the http body and therefore you have to set the content-type. But without body, no content type needed.

Aernout I also added a check after the API call for updating the values.
in case the json resultnis not pulled, ie var result is not updated, I now call the fetcher item to get a new token. for unknown reasons my token had expired and no new token was pulled prior expiry

1 Like

would you mind updating your code in this post? thanks. I was also trying to get the code completely reliable but today got an ‘access denied’ that I couldn’t really explain. but that could also be the Tedee service acting weird.

Hi Aernout,
I updated the rules in the first post.

In the token fetcher i added a check for “null”

if (tedee_token.state == null ) {
	 logInfo("tedee","API threw error, restarting in 5 minutes")
        createTimer(now.plusMinutes(5), [|
                tedee_tokenfetcher.sendCommand(ON)
        ])
}

and in the cron rule I changed the check slightly as per your suggestion:

if (result.equals("{}")) {
	tedee_tokenfetcher.sendCommand(ON)
}

	//in case of error, start new token fetch
val evaluation = transform("JSONPATH", "$.success", result)
if (evaluation == "false" ) {
	logInfo("tedee","API threw error, fetching token")
	tedee_tokenfetcher.sendCommand(ON)
} 
1 Like

Cheers guys,

API version 1.22 implemented a “PersonalKey” which can be made valid up to 5 years. This makes the whole approach hopefully somewhat less error prone (had several ocassions in the last couple of days when i did not have a valid token anymore
)

I updated the rules as following:

First rule still fetches a token and then requests the personal key with this token. The PAK is stored under /var/lib/openhab/secrets. (amend this location eventually for your system)



var String api_version = "1.22"
var String lock_id = "yr lockid" //EDIT THIS


rule "Retrieve Token"
when
	Item tedee_tokenfetcher changed from OFF to ON
then
var String api = "https://tedee.b2clogin.com/tedee.onmicrosoft.com/B2C_1_SignIn_Ropc/oauth2/v2.0/token"
var String result = ""

var String username = "your email" //EDIT THIS
var String password = "your password" //EDIT THIS

var String content = "grant_type=password&username=" + username + "&password=" + password + "&client_id=02106b82-0524-4fd3-ac57-af774f340979&scope=openid 02106b82-0524-4fd3-ac57-af774f340979&response_type=token"

result = sendHttpPostRequest(api, "application/x-www-form-urlencoded", content, 1000)
val evaluation = transform("JSONPATH", "$.error", result)

if (evaluation != "access_denied") {
	val token = transform("JSONPATH", "$.access_token", result)
	val time = transform("JSONPATH", "$.expires_in", result)
	logInfo("tedee", " Updated Token for tedee: " + token)
	tedee_token.postUpdate(token)
	// tedee_timer.postUpdate(time) // Timer no longer required as PAK has long validity
} else {
	logInfo("tedee","API threw error, restarting in 5 minutes")
	createTimer(now.plusMinutes(5), [|
		tedee_tokenfetcher.sendCommand(ON)
	])
}

if (tedee_token.state == null ) {
	 logInfo("tedee","API threw error, restarting in 5 minutes")
        createTimer(now.plusMinutes(5), [|
                tedee_tokenfetcher.sendCommand(ON)
        ])
}

// Request Personal Key PAK

api = "https://api.tedee.com/api/v" + api_version + "/personalaccesskey"
var String tokenheader = "Bearer " + tedee_token.state.toString
var String body = '{
    "name": "AlexPAK",   // provide a unique Name //EDIT THIS
	"validTo": "2023-12-31T00:00:00.197Z",  // set date - did not bother to make it variable from todays date
	"scopes": [
		"Device.Read",
		"Lock.Operate",
		"Organization.ReadWrite"
    ]
}' 

var headers = newHashMap("accept" -> "application/json","Authorization" -> tokenheader)
var String pakResult = ""
result = sendHttpPostRequest(api,"application/json-patch+json",body,headers,1000)
logInfo("tedee",result)
pakResult = transform("JSONPATH", "$.result.key", result)
logInfo("tedee",pakResult)
executeCommandLine(Duration.ofSeconds(5), "/etc/openhab/scripts/tedee.sh", pakResult)


end

Unfortunately Exec does not let me write directly to file, hence I needed the following script: (placed under /etc/openhab/scripts)

#!/bin/bash
touch /var/lib/openhab/secrets/tedee.pak
echo $1 > /var/lib/openhab/secrets/tedee.pak

Then I have two rules which read the PAK from the file and access the API (calls are more or less the same, small edits on the type of Authorization)

rule "Retrieve Lock status and update items"
when
	Time cron "0 0/30 * * * ?"
then

var String pak = executeCommandLine(Duration.ofSeconds(5), "cat", "/var/lib/openhab/secrets/tedee.pak")

var String api = "https://api.tedee.com/api/v" + api_version + "/my/lock/" + lock_id + "/sync"

var String tokenheader = "PersonalKey " + pak
var headers = newHashMap("accept" -> "application/json","Authorization" -> tokenheader)
var String result = "{}"

result = sendHttpGetRequest(api,headers,1000) 

if (result.equals("{}")) {
	tedee_tokenfetcher.sendCommand(ON)
}

	//in case of error, start new token fetch
val evaluation = transform("JSONPATH", "$.success", result)
if (evaluation == "false" || evaluation == "You are unauthorized." || result == "{}") {
	logInfo("tedee","API threw error, fetching token")
	tedee_tokenfetcher.sendCommand(ON)
} 

	val isConnected = transform("JSONPATH", "$.result.isConnected", result) 
	val state_int = transform("JSONPATH", "$.result.lockProperties.state", result)
	val batteryLevel = Integer::parseInt(transform("JSONPATH", "$.result.lockProperties.batteryLevel", result))
	logInfo("tedee", "Schloss ID " + lock_id + " ist verbunden (" + isConnected + "), aktueller Status " + state_int + " und Batterie " + batteryLevel + "%")


	


	//update Lockitems only if information has changed
	if (isConnected == "true" && tedee_isConnected != "Verbunden" ) {
		tedee_isConnected.postUpdate("Verbunden")
	} else {
		tedee_isConnected.postUpdate("Getrennt")
	}


	if (tedee_batteryLevel.state == NULL || batteryLevel != tedee_batteryLevel.state) {
		tedee_batteryLevel.postUpdate(batteryLevel)
	}
	
	if (tedee_state.state == NULL || state_int != tedee_state.state.toString) {
		tedee_state.postUpdate(state_int)
	}
	
	
		if (state_int == 2 || state_int == 6) {  // Updates visual representation of Lock in the Basic UI
			tedee_lockoperation.postUpdate(state_int)
		}
end

rule "tedee Bedienung"
when
	Item tedee_lockoperation received update
then
var String pak = executeCommandLine(Duration.ofSeconds(5), "cat", "/var/lib/openhab/secrets/tedee.pak")

var String tokenheader = "PersonalKey " + pak

var String api = "https://api.tedee.com/api/v" + api_version + "/my/lock/" + lock_id + "/operation"


var headers = newHashMap("accept" -> "application/json","Authorization" -> tokenheader)

switch(tedee_lockoperation.state) {
	case "2": {
		api = api + "/unlock"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(2)
	}

	case "6": {
		api = api + "/lock"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(6)
	}

	case "99": {
		api = api + "/unlock?mode=3"
		sendHttpPostRequest(api,"application/json-patch+json","",headers,1000)
		tedee_state.postUpdate(2)
	}
}
end
1 Like

In case anyone tried before: newest API is now also interpreting the soft open command correctly.
Means you can open the lock in the morning without pulling the spring.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.