Skip to content

Instantly share code, notes, and snippets.

@svyatogor
Last active April 26, 2025 14:03
Show Gist options
  • Save svyatogor/7839d00303998a9fa37eb48494dd680f to your computer and use it in GitHub Desktop.
Save svyatogor/7839d00303998a9fa37eb48494dd680f to your computer and use it in GitHub Desktop.
Convert SmartIR Broadlink commands to Tuya
@svyatogor
Copy link
Author

Huge shoutout to @mildsunrise for their research on the Tuya's format

@BenJamesAndo
Copy link

Thanks so much for creating this! Very helpful indeed.
I replaced print(process_commands(sys.argv[1])) with

output_filename = sys.argv[1].replace(".json", "_converted.json")
with open(output_filename, 'w') as output_file:
	output_file.write(process_commands(sys.argv[1]))

this way it outputs a converted file instead of printing the conversion in the terminal window.

@leviustinov
Copy link

leviustinov commented Mar 7, 2024

The converted codes work - tried using the mqtt.publish service:

service: mqtt.publish
data:
  payload: >-
    {"ir_code_to_send":"CIAifRGeAiUCYiABBIACbQaeIAMCgAJDIAsCiwZiYAcFQwK9AugBgBcAi6AbACVgF0ALwCNAK8BHAp4CBmA/gENAAYAjwDsCYgKqYEcEYgJPBtsgS0A3QHuAdwHsTUBXQAGAF4BjgAOAW8BT4AN7gE/gA2+AV4DDQEHgAa/gAavgAZPgAQdAiwK4m9shF8BDgJtA+0C7AIsgswHoAeEBA+ABG+EBD+ABT4FXgAeAi4BLwM9BN8BPQFtBNwCeIRdANYCzwHcB7E3gA7uBY+AB++ADY8AjAtsCySCn4QUnwD+Av0GfgaPgBbPBr8AfgAMBgAI="}
  topic: zigbee2mqtt/Living room - aircon/set

However, the converted JSON will now work - z2m gives an error of "Invalid message 'null', skipping...".
By me I need to send the full payload JSON data and escape the double quotes.

So for instance, I tried this just on the off command and this works.
The "heat" -> "low" -> "16" does not (z2m gives an error of "Invalid message 'null', skipping...".)

"commands": {
    "off": "{\"ir_code_to_send\": \"CIAinBFiAkMCYiABA4ACbQZACwCAYAMBbQZAB+ABDwAlICECiwaeoCMAniAPQBPgASPgAwuAP4BTAENgAcAbgDeAG0BPgCtAfwBtIFtAYwIoTiWggwKeAgbgAAeAG0ABgHvAC+ADVwJiAqqgcwAl4AgjwJ/ACwCe4AK34AGLgAMHYgJvnDcjfRHgAc9Ap8DvAYsGgS2Af4AJQSuAy+ADb8Ej4AWDgEvgAacABuACp0B7QAMAquAAb8FvA4ACzU3A70EtwXfAR4CbQJHhA6/gA6PhAU/gATvA7+ABE0CFwJfgARtAJ0Ib\"}",
    "heat": {
      "low": {
        "16": "GmIi2RFiAr0CBgIlAoACqgYlAgUHgAJPBkMCniATAZ4CQAdAGQBiIAUAYiAHACVgFwCAYBEAQyADQAUAQ2A/QB1AH0ADAGIgDwFCB0A/ACVgQUApAIAgBYA/A8kBBQdARQBiIG9AE4BHAMhgCwMlAmVOQDFAWUArQF2AH4B3wC+AE0AHQCuAmwBDIE/AFUA7gB9AqYBt4Ak7QEOAs+ABOwDIIQMCrJy9YRcAgKEDAIsgGwDnIRNAG4EP4AE7wEuA24AbgR2AtwC9YQOAe0EHQOeBK+ABz4FrACUhA4ETgGeA50AvQSFBF+AB/4BfgCPhAStAL8BvgHsB6AGBqUAFgK2AswdDAs8D9ADnBuAFg4DDwM9BT0D7APpiD0C/",

This is my configuration YAML in HA:

smartir:

climate:
  - platform: smartir
    name: Living room - aircon
    unique_id: ac_living_room
    device_code: 1343
    controller_data: zigbee2mqtt/Living room - aircon/set

Is there a way to modify this script to add this kind of output?
Much appreciated =).

I am a sysadmin and can do PowerShell and not python =).

@svyatogor
Copy link
Author

@leviustinov You need to specify the MQTT topic as zigbee2mqtt/Living room - aircon/set/ir_code_to_send, it will force z2m to put the payload in the correct field.

@leviustinov
Copy link

@leviustinov You need to specify the MQTT topic as zigbee2mqtt/Living room - aircon/set/ir_code_to_send, it will force z2m to put the payload in the correct field.

Works great, thanks!

In the meantime, knowing 0 python I ChatGPTed the fix for adding the strings and that works as well. Will be using what you mentioned though.

def encode_ir(command: str) -> str:
    signal = filter(get_raw_from_broadlink(base64.b64decode(command).hex()))

    payload = b''.join(pack('<H', t) for t in signal)
    compress_out = io.BytesIO()
    compress(compress_out, payload, level=2)
    compressed_payload = compress_out.getvalue()

    encoded_payload = base64.encodebytes(compressed_payload).decode('ascii').replace('\n', '')
    return "{\"ir_code_to_send\": \"" + encoded_payload + "\"}"

@rafareusch
Copy link

I'm having this error while trying to convert 1400.json

Traceback (most recent call last):
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 173, in
print(process_commands(sys.argv[1]))
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 167, in process_commands
data['commands'] = process_commands_recursively(data.get('commands', {}))
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 159, in process_commands_recursively
processed_commands[key] = encode_ir(value)
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 19, in encode_ir
compress(out := io.BytesIO(), payload, level = 2)
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 97, in compress
if (c := find_length()) and c[0] >= 3:
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 77, in
max(find_length_candidates(), key=lambda c: (c[0], -c[1]), default=None)
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 73, in
( (find_length_for_distance(pos - d), d) for d in distance_candidates() )
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 88, in distance_candidates
suffixes.insert(idx := find_idx(next_pos), next_pos)
File "/Users/rafaelreusch/Desktop/s/broadlink_to_tuya.py", line 82, in
find_idx = lambda n: bisect(suffixes, key(n), key=key)
TypeError: 'key' is an invalid keyword argument for bisect_right()

command: python broadlink_to_tuya.py 1400.json > 1400tuya.json
Using MacOS python 3.9.10

@BenJamesAndo
Copy link

@rafareusch the command should simply be
python broadlink_to_tuya.py 1400.json
remove > 1400tuya.json

@rafareusch
Copy link

@rafareusch the command should simply be python broadlink_to_tuya.py 1400.json remove > 1400tuya.json

Still with command python broadlink_to_tuya.py 1400.json I get the same error

@BenJamesAndo
Copy link

Try upgrading to Python 3.10 or newer. It seems the .py file isn't compatible with some older versions.
I'm running Python 3.11.6 and it works fine for me.

@rafareusch
Copy link

Try upgrading to Python 3.10 or newer. It seems the .py file isn't compatible with some older versions. I'm running Python 3.11.6 and it works fine for me.

You are correct!
Updated to python 3.12 and the script worked.

@rafareusch the command should simply be python broadlink_to_tuya.py 1400.json remove > 1400tuya.json

But this command is needed to direct the output to another file, and it works fine!

Thanks!! Awesome work you guys did!

@manherna
Copy link

Hi! I am trying to execute your script with python 3.13 and SmartIR file "1137.json". I am getting this error:

  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 174, in <module>
    print(process_commands(sys.argv[1]))
          ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 168, in process_commands
    data['commands'] = process_commands_recursively(data.get('commands', {}))
                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 162, in process_commands_recursively
    processed_commands[key] = process_commands_recursively(value)
                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 162, in process_commands_recursively
    processed_commands[key] = process_commands_recursively(value)
                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 162, in process_commands_recursively
    processed_commands[key] = process_commands_recursively(value)
                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 160, in process_commands_recursively
    processed_commands[key] = encode_ir(value)
                              ~~~~~~~~~^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 19, in encode_ir
    compress(out := io.BytesIO(), payload, level = 2)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 100, in compress
    emit_distance_block(out, c[0], c[1])
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/A200199775/tempo/broadlink_to_tuya.py", line 44, in emit_distance_block
    assert length < 256
           ^^^^^^^^^^^^
AssertionError 

Any clue why this may be happening?

@mildsunrise
Copy link

thanks, it was a little bug in my compression code. should be fixed in the latest revision of my gist.
@svyatogor please port these fixes into this gist when you have time :)

@manherna
Copy link

I managed to get it working using compression level 1 and 0. But 2 or 3 would result in the aforementioned error

@mildsunrise
Copy link

it is still failing even with the fix? in that case please send me a sample input and I'll take a deeper look

@manherna
Copy link

Hi! I ported the changes to @svyatogor 's code and it works with all levels of compression! New code would be:

import io
import base64
import json
import sys
from bisect import bisect
from struct import pack, unpack
from math import ceil

BRDLNK_UNIT = 269 / 8192

# MAIN API
filter = lambda x: [i for i in x if i<65535]

def encode_ir(command: str) -> str:
	# command = "JgC8AXE5DioPDg0PDQ8OKw0PDg4ODw0PDSsODw0rDisNDw0sDSsOKw0rDisNDwwQDSwMEA0QDBAMEAwQDRAMLAwtDSsOKwwQDBANEAwQDBANEAwQDBAMEA0QDBAMEAwQDRAMEAwQDRAMEAwQDBANEAwQDBAMEA4PDSsODw0PDQ8ODg4PDQ8NAAPNcjgOKg8ODg4ODg8qDg4PDQ8NDw4OKg8ODioPKg4ODykPKg8pDyoPKg4ODw0PKg4ODw0PDg4ODg4PDQ8ODg4PDQ8ODg4ODg8NDw4ODg8NDw0PDg4ODw0PDg4ODg4PDQ8qDw0PDQ8ODioPKg4ODyoODg8NDw0PDg4ODw0PDg4ODg4PDQ8ODg4PDQ8ODg4OKg8ODioPDQ8ODg4PDQ8ODg4ODg8NDw4ODg8NDw0PDg4ODw0PDg4ODg4PDQ8ODg4PDQ8ODg4ODg8NDw4ODg8NDw0PDg4ODw0PDg4ODg4PDQ8ODg4PDQ8NDw4ODg8NDw4ODg4ODw0PDg4ODg4PDQ8ODg4PKg4qDw0PDg4ODg4PDg4ODg4ODg8ODg4NDw4ODg8NDw0PDg8NDw0rDisNKw4rDg4OKw0rDgANBQAAAAAAAAAAAAAAAA=="
  signal = filter(get_raw_from_broadlink(base64.b64decode(command).hex()))

  payload = b''.join(pack('<H', t) for t in signal)
  compress(out := io.BytesIO(), payload, level = 2)
  payload = out.getvalue()
  return base64.encodebytes(payload).decode('ascii').replace('\n', '')


# COMPRESSION

def emit_literal_blocks(out: io.FileIO, data: bytes):
	for i in range(0, len(data), 32):
		emit_literal_block(out, data[i:i+32])

def emit_literal_block(out: io.FileIO, data: bytes):
	length = len(data) - 1
	assert 0 <= length < (1 << 5)
	out.write(bytes([length]))
	out.write(data)

def emit_distance_block(out: io.FileIO, length: int, distance: int):
	distance -= 1
	assert 0 <= distance < (1 << 13)
	length -= 2
	assert length > 0
	block = bytearray()
	if length >= 7:
		assert length - 7 < (1 << 8)
		block.append(length - 7)
		length = 7
	block.insert(0, length << 5 | distance >> 8)
	block.append(distance & 0xFF)
	out.write(block)

def compress(out: io.FileIO, data: bytes, level=2):
	'''
	Takes a byte string and outputs a compressed "Tuya stream".
	Implemented compression levels:
	0 - copy over (no compression, 3.1% overhead)
	1 - eagerly use first length-distance pair found (linear)
	2 - eagerly use best length-distance pair found
	3 - optimal compression (n^3)
	'''
	if level == 0:
		return emit_literal_blocks(out, data)

	W = 2**13 # window size
	L = 255+9 # maximum length
	distance_candidates = lambda: range(1, min(pos, W) + 1)

	def find_length_for_distance(start: int) -> int:
		length = 0
		limit = min(L, len(data) - pos)
		while length < limit and data[pos + length] == data[start + length]:
			length += 1
		return length
	find_length_candidates = lambda: \
		( (find_length_for_distance(pos - d), d) for d in distance_candidates() )
	find_length_cheap = lambda: \
		next((c for c in find_length_candidates() if c[0] >= 3), None)
	find_length_max = lambda: \
		max(find_length_candidates(), key=lambda c: (c[0], -c[1]), default=None)

	if level >= 2:
		suffixes = []; next_pos = 0
		key = lambda n: data[n:]
		find_idx = lambda n: bisect(suffixes, key(n), key=key)
		def distance_candidates():
			nonlocal next_pos
			while next_pos <= pos:
				if len(suffixes) == W:
					suffixes.pop(find_idx(next_pos - W))
				suffixes.insert(idx := find_idx(next_pos), next_pos)
				next_pos += 1
			idxs = (idx+i for i in (+1,-1)) # try +1 first
			return (pos - suffixes[i] for i in idxs if 0 <= i < len(suffixes))

	if level <= 2:
		find_length = { 1: find_length_cheap, 2: find_length_max }[level]
		block_start = pos = 0
		while pos < len(data):
			if (c := find_length()) and c[0] >= 3:
				emit_literal_blocks(out, data[block_start:pos])
				emit_distance_block(out, c[0], c[1])
				pos += c[0]
				block_start = pos
			else:
				pos += 1
		emit_literal_blocks(out, data[block_start:pos])
		return

	# use topological sort to find shortest path
	predecessors = [(0, None, None)] + [None] * len(data)
	def put_edge(cost, length, distance):
		npos = pos + length
		cost += predecessors[pos][0]
		current = predecessors[npos]
		if not current or cost < current[0]:
			predecessors[npos] = cost, length, distance
	for pos in range(len(data)):
		if c := find_length_max():
			for l in range(3, c[0] + 1):
				put_edge(2 if l < 9 else 3, l, c[1])
		for l in range(1, min(32, len(data) - pos) + 1):
			put_edge(1 + l, l, 0)

	# reconstruct path, emit blocks
	blocks = []; pos = len(data)
	while pos > 0:
		_, length, distance = predecessors[pos]
		pos -= length
		blocks.append((pos, length, distance))
	for pos, length, distance in reversed(blocks):
		if not distance:
			emit_literal_block(out, data[pos:pos + length])
		else:
			emit_distance_block(out, length, distance)

def get_raw_from_broadlink(string):
  dec = []
  unit = BRDLNK_UNIT  # 32.84ms units, or 2^-15s
  length = int(string[6:8] + string[4:6], 16)  # Length of payload in little endian

  i = 8
  while i < length * 2 + 8:  # IR Payload
    hex_value = string[i:i+2]
    if hex_value == "00":
      hex_value = string[i+2:i+4] + string[i+4:i+6]  # Quick & dirty big-endian conversion
      i += 4
    dec.append(ceil(int(hex_value, 16) / unit))  # Will be lower than initial value due to former round()
    i += 2

  return dec


def process_commands(filename):
  with open(filename, 'r') as file:
    data = json.load(file)

  def process_commands_recursively(commands):
    processed_commands = {}
    for key, value in commands.items():
      if isinstance(value, str):
        processed_commands[key] = encode_ir(value)
      elif isinstance(value, dict):
        processed_commands[key] = process_commands_recursively(value)
      else:
        processed_commands[key] = value

    return processed_commands

  data['commands'] = process_commands_recursively(data.get('commands', {}))
  data['supportedController'] = 'MQTT'
  data['commandsEncoding'] = 'Raw'
  return json.dumps(data, indent=2)


print(process_commands(sys.argv[1]))

@gianlucasullazzo
Copy link

gianlucasullazzo commented Aug 21, 2024

Hi guys, is there a way to do the opposite? I changed my IR blaster from tuya to broadlink and I need some solution to convert code from old format to the new one.

@MusiCode1
Copy link

This is so genius!
Why is this code not integrated into SmartIR?

@BenJamesAndo
Copy link

This is so genius! Why is this code not integrated into SmartIR?

I did see on the SmartIR fork that the author is thinking about making a dynamic converter litinoveweedle/SmartIR#122 (comment)

@nazmibojan
Copy link

Hi guys, I got this error when convert 4180.json

Traceback (most recent call last):
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 173, in <module>
    print(process_commands(sys.argv[1]))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 167, in process_commands
    data['commands'] = process_commands_recursively(data.get('commands', {}))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 159, in process_commands_recursively
    processed_commands[key] = encode_ir(value)
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 16, in encode_ir
    signal = filter(get_raw_from_broadlink(base64.b64decode(command).hex()))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 145, in get_raw_from_broadlink
    dec.append(ceil(int(hex_value, 16) / unit))  # Will be lower than initial value due to former round()
ValueError: invalid literal for int() with base 16: ''

Do you have any idea why did this happened?

@nazmibojan
Copy link

Hi guys, I got this error when convert 4180.json

Traceback (most recent call last):
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 173, in <module>
    print(process_commands(sys.argv[1]))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 167, in process_commands
    data['commands'] = process_commands_recursively(data.get('commands', {}))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 159, in process_commands_recursively
    processed_commands[key] = encode_ir(value)
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 16, in encode_ir
    signal = filter(get_raw_from_broadlink(base64.b64decode(command).hex()))
  File "/home/nazmi/Documents/ferbos/code/irremote/broadlink_to_tuya.py", line 145, in get_raw_from_broadlink
    dec.append(ceil(int(hex_value, 16) / unit))  # Will be lower than initial value due to former round()
ValueError: invalid literal for int() with base 16: ''

Do you have any idea why did this happened?

Apologize for the mistake, 4180.json is using Xiaomi controller, not broadlink.

@nakata5321
Copy link

Hi guys, thank you for your code!
I came form SmartIR repo to here and i'm working with Z06/UFO-R11 and convert to this IR remote from broadlink. To work properly, i have to change one line at the and of the code:
data['supportedController'] = 'MQTT' -> data['supportedController'] = 'UFOR11'
Just leave it here for the history.

@glenricky
Copy link

Hi everyone, I am not a programmer at all, but just playing with homeassistant and wants to use all my devices locally. I am using tuya and tuya local and I can see all my devices. However the IR devices doesnt work and I just found about smartir. I have broadlink IR blaster and it works with smartir, but my tuya device isn't (I have 2 identical AC in seperate floor) and then I found out about this converter which I think should help me so that smartir can send the right command with the tuya device. My question is, how do I use this tool? can anyone give me step-by-step? My AC works with 1300.json.

@pasthev
Copy link

pasthev commented Mar 3, 2025

Hi @glenricky,

https://irtuya.streamlit.app/ might help in your case.
@svyatogor sorry, I don't mean to hack this thread - just thought that an online tool can be useful for non-programmers.

Pascal

@LotharWoman
Copy link

LotharWoman commented Apr 26, 2025

Hello folks,
Unfortunately I have no idea about the matter. However, I would like to operate air conditioning in the "Followme" mode with an external temperature sensor. The remote control sends the determined value of the room temperature to the air conditioning every 2-3min. Unfortunately, it can only be placed unfavorably and the batteries empty very quickly.
I have already managed to intercept some codes required with "Learn Command" in home assistant. Unfortunately, they are only graded in 1 ° C steps and the air conditioning is not very finely regulated.
I hope for your help. Is someone able to decode the transmitted temperature value from attached codes and to create new complete codes in 0.2 ° C steps? You would help me and others a lot.

Thanks for your help.

    "FollowMe 19°C": "dRHoEDICOgZSAtQBUgIaBlICGgZSAhoGUgLUAVICGgYzAvQBUgL0AVEC/AVRAvUBUQLVAVEC9QFRAhsGMgL0AVICGgYyAjoGMwIaBlIC9AFRAhoGMwL0AVEC9AFSAvsFMgI6BjMCEwJSAtQBUgIaBjIC9AFSAhoGMwI5BjMC9AEyAhMCMgL0ATMCOgYyAjoGMwI5BjMCGwZRAhsGMgI6BjICEwIzAhoGUgL0ATIC9AEyAhQCMgL0ATIC9AFSAvQBMwI5BjMCQxQ1ERcRMgI4BjMC8wEyAjkGMgI4BjICOQYyAhMCEwJYBhMCEwISAjMCEgI5BjICEwITAhMCMgITAhMCWAYTAhICEwJYBhMCWAYTAlgGEwITAhMCWAYTAjIC9AEyAhMCWAYSAlgGEwISAhMCMgITAjkGEwIyAhMCWAYTAlcGEwITAhMCMgL0ATICEwJZBhMCWAYTAlcG9AFYBhMCdwbzAXcG9AEyAhMCWAb0AVEC9AEyAvQBUQL0ATIC9AEyAhICMgL0AXcG9AEwdQ==",
    "FollowMe 20°C": "ixHoEDECWQYTAhMCMgI4BhMCWQYUAlcGEwITAjMCOAYyAhQCEgITAhQCWAYSAhMCMwITAhMCEwITAlsGEAITAjUCOAYxAjoGEgJYBhQCEgIyAjoGMgISAhUCWAYSAhMCFAISAjICEwIUAhICMgI6BhICMwISAjoGMQITAhQCWAYWAlgGEgISAhICWQYTAhMCMwI4BhMCWQYSAloGEgJYBhMCEwITAlkGEwIUAjECOQYUAjECEwIUAhICMgIUAhICEwJZBhMCQBRUEfYQNAJXBhICFQISAlgGFAJXBhMCWAYUAhICEwJYBhUCMQIVAhICEgJaBhICEgIVAhICNALxATMCOwYvAvUBUwIYBjICOQYzAjsGMAL0AXAC+wUyAhQCUQIZBjMC9AFRAtUBcALWAVEC1AFSAhkGMgIUAlEC/QVPAvQBUQIaBjICOQY1AvIBUQIbBjEC9AFTAhgGMgI6BjICOQY0AjgGMQL2ATECOAY0AvMBMQI6BjICEgI0AvMBMgIUAjIC8wE0AjcGMwIwdQ==",
    "FollowMe 21°C": "eBECETMCOQYVAjICFAI5BjQCOAYTAlsGEgITAjQCOAY1AhECEwIVAhICWAYVAhICFQIxAhMCFAITAloGEgITAjQCOQYVAlcGEwJaBhMCFAITAlsGEwISAjICOgYUAjICEwI7BjICFQISAhMCEwJaBhMCFAIyAjsGEgIzAhQCOAYzAhQCFQISAhICWQYTAjUCEQI5BjQCOQYVAlcGEwJaBhMCFAITAlkGFAITAjQCOAYTAjMCEwIUAhUCEgITAjMCEgJcBhICRRQ2ERYRNQI4BjICFAITAlsG8gFcBjACOgYSAjQCEgI6BjMCFQIRAhMCFQJYBhMCEwIzAhUCEQITAhQCWAYUAjICFgI4BhMCWQYTAloGEgITAjQCOQYUAjICFAI5BjMCFQISAlgGFAITAvYBMAIzAjoGEwIVAjECOQY0AvMBNAI4BjUC8wEyAhQCMgIcBlEC8wFTAhoGUQL9BVICGAZSAhsGUgL0AVIC/AUyAhYCMAI5BjQC8wFSAtUBUQL3ATAC9AE0AjgGMgIwdQ==",
    "FollowMe 22°C": "dBEIETICWwYSAhMCFAJZBhUCWQYSAjoGMwITAhUCWAYTAhYCEgITAjICOgYTAjUCEgITAhMCFAIyAjwGEgIyAhMCOwYxAjsGEwJZBhMCFAIyAjwGEgIyAhQCOgYyAjoGEwI1AhECFAITAhUCMQI8BhECMwIUAjkGNAISAhMCFAITAlkGFAITAjICOwYSAjQC8wFbBjICPAYSAlgGEwJZBhUCEgITAloGEgIVAjICPAYQAjMCEwIUAhMCEwI1AhECEwJaBhMCRBQ3ERsRMgI8BhICMgIUAjsGNQI4BhYCWAYWAhICMgI7BhQCMwITAhUC9AF5BvUBMgIWAhICNAITAhYCVwYWAhICEwJbBhMCOwYzAjsGFAI0AhICPAYyAhMCFQJbBhICPAYSAhcCMgLzATICFAIzAhsGNAITAjMCHQZRAvUBUgLXAVECHAZRAtUBUgIeBlAC9AFUAvsFMgI8BjQCOgY0AjoGUwLUAVQCGgZUAtMBUgIcBjYC8gFSAvQBUwLVAVIC1gFRAhwGMwIwdQ==",
    "FollowMe 23°C": "iRHoEDUCOAZRAtcBTwIbBlMCGQZRAvsFcQLVAVICGgZSAtcBUQLTAXMC+gVUAvIBUQLWAVIC0wFxAvsFUgL1AVECHAZQAvsFcQL6BVMC9AFRAv0FUQLzAVMCGQZVAvkFcQL8BVAC9gFRAtQBUwIaBjQC8wFVAhkGNQLyAVIC9QFRAtUBVALzAVIC/AU1AjgGNgI4BjUCOQYyAhsGNAI7BjIC9QFxAvwFMwIVAlEC1wFPAtUBcwLUAVMC1AFUAvMBMgIcBjMCRhQ6ERgRNAI8BjEC9AEzAjoGMwI7BjMCOgYzAvQBNQI5BjQC8wEzAvQBMwI7BjICFQIyAvQBMwL1ATICOwYzAhMCNQIZBjUCOQY0AjkGNQLzATICOwY1AhICEwJaBhMCWgYUAjsGMgIUAhMCFQITAloGEwIWAjECPAYSAjMCEwIUAhUCEgITAjMCFAJaBhMCPAYTAlsGEwJbBhMCWQYVAjkGFQIxAhYCWAb2ATICEgIVAhMCNQISAhMCFAIyAvYBMgITAloGEwIwdQ==",
    "FollowMe 24°C": "lhHpEDACOQY0AhMCUQL6BVQCGAYzAjkGMwL2AU8CGgY0AhICUgLVAVECGgY1AvMBUAL2AVEC0wFUAhgGMwL0AVECHAYyAjgGMwI5BjUC8gEyAjoGMgI4BjQC8wEyAhUCMQLzATUCEAIzAvUBMgI7BjEC8wE1AhICMQIbBlECHQYwAjoGMQL2AVECGQY0AjgGMwI5BjICOwYzAjcGMgIaBlIC9gEwAjkGNQLyATICFQIxAvMBNALzATICFAIyAvYBMQI5BjQCQBQ2ETURFQJYBhICEwIUAlgGFgJXBhICOgYyAhMCEwJYBhQCEwIUAjICEwI5BhMCNQIRAhMCEwI0AhICWAb2ATACEwJaBhICWwYRAjkGFQIxAhMCWQYTAjsGEgIyAhQCEgITAjQCEgITAhQCMQL0AVgGFAIyAhUCEgITAlkGEwJYBhUCVwYTAhQCEwJYBhYCVQYTAlkGEwJZBvYBVwYSAnkG8wEyAvUBdwb1ATICEwIzAvMBMgL0AVIC9gEwAvQBVALyAVgGFQIwdQ==",
    "FollowMe 25°C": "lBHoEDMCOQZUAtMBUQIbBlECHQZSAhgGUQLVAVMCGQZSAtYBUAL0AVQC+AVxAtYBUQLUAVIC9AFRAv0FbwLUAVQCGAZSAhoGUwL6BXIC1AFRAhsGUgL8BXAC1gFRAtQBUwIZBlEC1gFxAtQBUwIZBlEC1AFSAtUBcwL5BTMCOgYyAhMCVALTAVECGwZRAhkGNAIZBlICGQYyAjoGMwI6BjIC9QFQAh0GMAITAlMC0wFSAtUBUgL0AVEC1AFUAvIBVAL5BTICQxRVEfYQVAIYBjMCFAIxAhkGUgIbBjICPAYvAhQCNAIZBjICFgIwAvQBNAI4BjIC9QExAhMCMgL0ATICOgYzAvUBMgI4BjQCOQY0AjkGNQLyATMCOwYyAlkGFAITAhQCFQIyAjkGFgISAjICFAITAlkGFQISAhMCFAIzAjsGEgJbBhMCNAISAhMCFAJZBhQCOgYTAlsGEgJaBhMCWQYUAloGFAITAhUCWAYWAhACFAIzAhMCFgISAhMCFAIyAhMCFAITAlsGEgIwdQ==",
    "FollowMe 26°C": "khHnEDICOgZRAtUBUgIaBlICGgZSAhoGUQLVAVICGgZRAtUBcQLUAVICGgZSAtQBUgLVAXEC1AFSAhoGUgLUAVICGwZRAhoGUgL7BXAC1QFSAhoGUQIaBlIC1QFRAhoGUgLVAVEC9AFSAtQBUgIaBlIC1AFSAvQBUgL7BXEC1AFSAhsGUQLVAVECGgZSAhoGUgIaBlIC+wVxAvsFUQIbBlEC1QFxAvsFUQL0AVIC1QFSAtQBcQLVAVEC1QFSAvMBUgIaBjMCIxR0EdgQUQIbBjICEwJSAhoGMgIbBlECGgYzAhMCUgL6BTMCEwJSAtUBUQIaBjMC9AFRAvQBUgLVAVICGwYzAvQBMwI6BjMCOgYyAjsGMgL0ATMCOgYzAjoGMwL0ATICOwYyAvQBMwIUAjMC8wEzAjoGMwL0ATMCFAIyAhsGUgL0ATMCOwYzAvQBMgI7BjMCOgYzAhsGMwI6BjMCOgYzAjsGMgL0ATMCOgYzAvQBMwITAjMCEwIUAhMCEwIzAhQCEwITAloGFAIwdQ==",
    "FollowMe 27°C": "hRHlEDICOQZRAtUBUQIZBlICGQZRAhkGUgLUAVECGgZRAvQBUQLVAVECGQYyAvQBUQL0AVEC1AFSAhkGMgL0AXAC+gUyAjgGMwI4BjIC9AFwAvsFMgI4BjICEwJRAvoFUgIZBjICEwJRAtUBUQIZBjIC9AFwAtUBUQIZBjIC9AEyAhMCMgLzATICOQYxAjkGMgI5BjICGQZRAhkGMgI4BjICEwIzAhkGUgL0ATIC9AEzAhMCMwL0ATICEwIzAvQBMgI5BjICQhQ1ERYRMwI5BjIC9AEzAjkGMwI5BjICOgYTAjMCEwI5BjMCEwITAhMCEwJZBhMCEwIzAhMCEwITAhMCWQYTAjMCEwI5BhMCWQYTAlkGEwITAhMCWQYTAlgGEwIUAhMCWQYTAlgGFAIyAvQBMgITAlgGEwIUAhMCMgITAjoGEwIzAhMCEwITAjICEwI6BhMCWQYTAncG9AF4BvQBeAb0AVgGEwIyAvQBeAb0ATMC8wEzAhMCMgL0ATMC9AFRAvQBMgL0AXgG8wEwdQ=="

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment