Skip to content

Instantly share code, notes, and snippets.

@dogtopus
Last active May 27, 2025 08:15
Show Gist options
  • Save dogtopus/7a25ba0763bc4a9c6e5251a74cb3ad78 to your computer and use it in GitHub Desktop.
Save dogtopus/7a25ba0763bc4a9c6e5251a74cb3ad78 to your computer and use it in GitHub Desktop.
Hitachi PC-LH series remote code decoder. Takes Flipper timing format.
#!/usr/bin/env python3
import io
import itertools
KEYCODE_NAMES = {
0x01: 'POWER',
0x04: 'MODE',
0x05: 'FAN_SPEED',
0x08: 'TEMP_DOWN',
0x09: 'TEMP_UP',
0x0a: 'TEST',
0x0b: 'TIMER_SET',
0x0d: 'TIMER_RESET',
0x0e: 'SWING',
0x0f: 'FILTER_RESET',
}
FAN_SPEED_NAMES = {
0x1: 'Low',
0x2: 'Medium',
0x4: 'High',
}
MODE_NAMES = {
0x2: 'Fan',
0x4: 'Dehumidifying',
0x6: 'Cooling',
0x8: 'Heating',
0xb: 'Auto',
}
SWING_NAMES = {
0x0: '0',
0x2: '1',
0x4: '2',
0x6: '3',
0x8: '4',
0xa: '5',
0xc: '6',
0x1: 'Swing',
}
POWER_NAMES = {
0x9: 'ON',
0xa: 'OFF'
}
def checksum(data: bytes) -> str:
if all(d[0] ^ d[1] == 0xff for d in itertools.batched(data, 2)):
return '(OK)'
return '(NG)'
def simplify(data: bytes) -> str:
if data[:3] != b'\x01\x10\x00':
return
print('================')
print('Raw:', data[3::2].hex())
def parse(data: bytes):
def parse_temp(dio: io.BytesIO):
temp_bytes = dio.read(2)
temp = 7 + (temp_bytes[0] >> 1)
print(f'Temperature: {temp_bytes[0]:#04x}', checksum(temp_bytes))
# TODO is the last bit indicating the unit?
print(f' Decoded: {temp} degC')
def parse_fsm(dio: io.BytesIO):
fan_speed_mode_bytes = dio.read(2)
fan_speed = FAN_SPEED_NAMES.get(fan_speed_mode_bytes[0] >> 4, '?')
mode = MODE_NAMES.get(fan_speed_mode_bytes[0] & 0xf, '?')
print(f'Fan speed & mode: {fan_speed_mode_bytes[0]:#04x}', checksum(fan_speed_mode_bytes))
print(f' Fan speed: {fan_speed}')
print(f' Mode: {mode}')
def parse_swing(dio: io.BytesIO):
swing_bytes = dio.read(2)
print(f'Swing: {swing_bytes[0]:#04x}', checksum(swing_bytes))
print(f' Angle: {SWING_NAMES.get(swing_bytes[0], '?')}')
def parse_control(dio: io.BytesIO):
control_bytes = dio.read(2)
power = control_bytes[0] >> 4
print(f'Control: {control_bytes[0]:#04x}', checksum(control_bytes))
print(f' Power: {POWER_NAMES.get(power)}')
def parse_timer(dio: io.BytesIO):
timer_bytes = dio.read(6)
timer_raw = int.from_bytes(timer_bytes[::2], 'little')
timer_off = timer_raw & 0xfff
timer_on = timer_raw >> 12
print(f'Timer: {timer_raw:#08x}', checksum(timer_bytes))
print(f' Off timer: {timer_off} min')
print(f' On timer: {timer_on} min')
def parse_timer_test(dio: io.BytesIO):
timer_bytes = dio.read(4)
timer_raw = int.from_bytes(timer_bytes[::2], 'little')
timer_off = timer_raw & 0xfff
mode = MODE_NAMES.get(timer_raw >> 12, '?')
print(f'Timer & mode (test): {timer_raw:#06x}', checksum(timer_bytes))
print(f' Off timer: {timer_off} min')
print(f' Mode: {mode}')
def parse_parameter_marker(dio: io.BytesIO):
parameter_marker_bytes = dio.read(2)
print(f'Parameter marker? (0x3f): {parameter_marker_bytes[0]:#04x}', checksum(parameter_marker_bytes))
dio = io.BytesIO(data)
preamble = dio.read(3)
if preamble != b'\x01\x10\x00':
print('Invalid frame.')
return
print('----------------')
sync2_bytes = dio.read(4)
sync2 = int.from_bytes(sync2_bytes[::2], 'little')
print(f'SYNC 2 (0xff40): {sync2:#06x}', checksum(sync2_bytes))
# If this is 0xcc, it could be an RAS series remote and not compatible with LH series remote
# https://perhof.wordpress.com/2015/03/29/reverse-engineering-hitachi-air-conditioner-infrared-remote-commands/
size_bytes = dio.read(2)
type = 'UNKNOWN'
size = 0
if size_bytes[0] == 0xcc:
type = 'RAS'
size = 0xf
return
elif size_bytes[0] & 0xf0 == 0xe0:
type = 'LH'
size = (size_bytes[0] & 0x0f) + 1
else:
print(f'WARNING: Unknown remote type {size_bytes:#04x}.')
return
print(f'Size & type: {size_bytes[0]:#04x}', checksum(size_bytes))
print(f' Type: {type}')
print(f' Size: {size}')
keycode_marker_bytes = dio.read(2)
print(f'Keycode marker? (0x89): {keycode_marker_bytes[0]:#04x}', checksum(keycode_marker_bytes))
keycode_bytes = dio.read(2)
keycode = keycode_bytes[0] & 0x7f
side = keycode_bytes[0] >> 7
print(f'Keycode: {keycode:#04x} ({KEYCODE_NAMES.get(keycode, '?')})', checksum(keycode_bytes))
print(f' Side: {('A', 'B')[side]}')
if size <= 2:
return
if size == 9:
if keycode == 0x0b:
parse_parameter_marker(dio)
parse_temp(dio)
parse_timer(dio)
parse_fsm(dio)
parse_swing(dio)
elif size == 7:
if keycode == 0x01:
parse_parameter_marker(dio)
parse_temp(dio)
parse_fsm(dio)
parse_swing(dio)
parse_control(dio)
elif size == 6:
if keycode == 0x04:
parse_parameter_marker(dio)
parse_temp(dio)
parse_fsm(dio)
parse_swing(dio)
elif size == 5:
if keycode == 0x0a:
parse_temp(dio)
parse_timer_test(dio)
elif size == 4:
parse_parameter_marker(dio)
match keycode:
case 0x0e:
parse_swing(dio)
case 0x08 | 0x09:
parse_temp(dio)
case 0x05:
parse_fsm(dio)
elif size != 3:
print("WARNING: Don't know how to parse parameters.")
if __name__ == '__main__':
while True:
try:
a = input().rstrip()
if a == '':
break
except EOFError:
break
b = list(int(i) for i in a.split(' '))
c = list(itertools.batched(b, 2))
d = bytes(int(''.join('1' if i[1] / i[0] > 2.0 else '0' for i in reversed(j)), 2) for j in itertools.batched(c, 8))
simplify(d)
parse(d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment