├── .env.example ├── .gitignore ├── README.md ├── __init__.py ├── images ├── command_transform.jpeg ├── light.jpg ├── panasonic.jpeg ├── running_mode.jpg └── toplogy.jpg ├── panasonic.py ├── requirements.txt ├── server.py ├── setup.sh └── supervisor.conf /.env.example: -------------------------------------------------------------------------------- 1 | PANASONIC_SSID= 2 | PANASONIC_ACW_TC= 3 | PANASONIC_USER_ID= 4 | 5 | PANASONIC_DEVICE_ID= 6 | PANASONIC_TOKEN= 7 | 8 | PANASONIC_AIR_CONDITION_DEVICE_ID= 9 | PANASONIC_AIR_CONDITION_TOKEN= 10 | 11 | BEMFA_CLIENT_ID= 12 | BEMFA_TOPICS=xxx001,xxx002 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .env 3 | __pycache__/ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A stupid way to extend miot/xiaoai. 2 | 3 | ### Demo for Panasonic Bath Heater `FV-RB20VL1` 4 | 5 | 6 | 7 | 1. 逆向 `Panasonic Smart China`,获得控制浴霸的请求信息(HTTP 请求),详见 `apps/panasonic.py`; 8 | 9 | 2. 通过米家 APP 添加 `巴法`,以支持第三方设备,实现监听巴法云的 MQTT 10 | 消息,巴法云目前不支持浴霸,需要使用其他设备进行替代并映射,因灯与运行模式完全独立,建议实现两个设备,详见 `server.py`; 11 | 12 | 13 | 3. 为了更好的体验,需要在小爱音箱 APP 中,创建训练,转换浴霸指令为巴法云支持的指令。 14 | 15 | 16 | ### Installation: 17 | 18 | `copy .env.example .env` 19 | 20 | `python3 -m venv venv` 21 | 22 | `source venv/bin/activate` 23 | 24 | `pip install -r requirements.txt` 25 | 26 | ### Run server: 27 | 28 | `python apps/server.py` 29 | 30 | ### Changelog 31 | 32 | 2024-06-29 33 | 34 | 1. 重构代码,新增松下空调 35 | 2. replace config.py with dotenv 36 | 3. 小爱训练计划中已支持第三方设备(巴法云)展示与命令映射,配置体验稍有提升 37 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/__init__.py -------------------------------------------------------------------------------- /images/command_transform.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/images/command_transform.jpeg -------------------------------------------------------------------------------- /images/light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/images/light.jpg -------------------------------------------------------------------------------- /images/panasonic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/images/panasonic.jpeg -------------------------------------------------------------------------------- /images/running_mode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/images/running_mode.jpg -------------------------------------------------------------------------------- /images/toplogy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsk/miot-http-support/16fe488664a04417ad779d86ca9c3738961c5453/images/toplogy.jpg -------------------------------------------------------------------------------- /panasonic.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | 9 | class Panasonic(object): 10 | deviceId = '' 11 | token = '' 12 | url = '' 13 | params = {} 14 | headers = { 15 | 'Host': 'app.psmartcloud.com', 16 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 17 | 'X-Requested-With': 'XMLHttpRequest', 18 | 'Accept-Language': 'en-US,en;q=0.9', 19 | # 'Accept-Encoding': 'gzip, deflate, br', 20 | 'Content-Type': 'application/json', 21 | 'Origin': 'https://app.psmartcloud.com', 22 | 'xtoken': f'SSID={os.getenv("PANASONIC_SSID", "")}', 23 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148', 24 | # 'Referer': '', # no need 25 | # 'Content-Length': '468', # no need 26 | 'Connection': 'keep-alive', 27 | } 28 | cookies = { 29 | 'acw_tc': os.getenv("PANASONIC_ACW_TC", ""), 30 | } 31 | 32 | def update_params(self, action): 33 | pass 34 | 35 | def trigger(self): 36 | data = { 37 | "id": 1, 38 | "usrId": os.getenv("PANASONIC_USER_ID", ""), 39 | "deviceId": self.deviceId, 40 | "token": self.token, 41 | "params": self.params, 42 | } 43 | # import pdb; 44 | # pdb.set_trace() 45 | response = requests.post(self.url, headers=self.headers, cookies=self.cookies, json=data, verify=False) 46 | print(response.json()) 47 | 48 | def control(self, action=''): 49 | self.update_params(action) 50 | self.trigger() 51 | 52 | 53 | class BathHeater(Panasonic): 54 | deviceId = os.getenv("PANASONIC_DEVICE_ID", "") 55 | token = os.getenv("PANASONIC_TOKEN", "") 56 | url = 'https://app.psmartcloud.com/App/ADevSetStatusInfoFV54BA1C' 57 | params = { 58 | "DIYnextwindDirectionSet": 255, 59 | "DIYnextwindKindSet": 255, 60 | "warmTempset": 255, 61 | "DIYnextWarmTempset": 255, 62 | "DIYnextStepNo": 2, 63 | "windDirectionSet": 255, 64 | "timeSet": 3, # 默认15分钟 65 | "DIYnextRunningMode": 255, 66 | "windKindSet": 255, 67 | "DIYnextTimeSet": 255, 68 | } 69 | 70 | def update_params(self, action): 71 | running_modes = { 72 | '打开灯': 1, 73 | '关闭灯': 0, 74 | '换气': 38, 75 | '取暖': 37, 76 | '凉干燥': 40, 77 | '热干燥': 42, 78 | '待机': 32, 79 | } 80 | light_set = running_modes[action] if action in ['打开灯', '关闭灯'] else 255 81 | running_mode = running_modes[action] if action in ['换气', '取暖', '凉干燥', '热干燥', '待机'] else 255 82 | self.params['runningMode'] = running_mode 83 | self.params['lightSet'] = light_set 84 | 85 | 86 | class AirCondition(Panasonic): 87 | deviceId = os.getenv("PANASONIC_AIR_CONDITION_DEVICE_ID", "") 88 | token = os.getenv("PANASONIC_AIR_CONDITION_TOKEN", "") 89 | url = 'https://app.psmartcloud.com/App/ACDevSetStatusInfoAW' 90 | params = { 91 | 'runMode': 3, 92 | 'forceRunning': 0, 93 | 'remoteForbidMode': 0, 94 | 'remoteMode': 0, 95 | 'setTemperature': 54, 96 | 'setHumidity': 128, 97 | 'windSet': 3, 98 | 'exchangeWindSet': 0, 99 | 'portraitWindSet': 15, 100 | 'orientationWindSet': 13, 101 | 'nanoeG': 0, 102 | 'nanoe': 0, 103 | 'ecoMode': 0, 104 | 'muteMode': 1, 105 | 'filterReset': 0, 106 | 'powerful': 0, 107 | 'powerfulMode': 0, 108 | 'thermoMode': 0, 109 | 'buzzer': 1, 110 | 'autoRunMode': 3, 111 | 'unusualPresent': 0, 112 | 'runForbidden': 0, 113 | 'inhaleTemperature': 27, 114 | 'outsideTemperature': 32, 115 | 'insideHumidity': 255, 116 | 'alarmCode': 'F093', 117 | 'nanoeModule': 0, 118 | 'TDWindModule': 0, 119 | } 120 | 121 | def update_params(self, action): 122 | running_modes = { 123 | 'on': 1, 124 | 'off': 0, 125 | } 126 | self.params['runStatus'] = running_modes[action] 127 | 128 | 129 | if __name__ == '__main__': 130 | device = BathHeater() 131 | device.control('打开灯') 132 | # device.control('关闭灯') 133 | exit() 134 | device = AirCondition() 135 | device.control('on') 136 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | paho-mqtt 3 | python-dotenv 4 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import paho.mqtt.client as mqtt 4 | from dotenv import load_dotenv 5 | 6 | from panasonic import AirCondition, BathHeater 7 | 8 | load_dotenv() 9 | 10 | 11 | def on_connect(client, userdata, flags, rc): 12 | print("Connected with result code " + str(rc)) 13 | for topic in os.getenv("BEMFA_TOPICS", "").split(","): 14 | client.subscribe(topic) 15 | 16 | 17 | def on_message(client, userdata, msg): 18 | print(msg.topic + " " + str(msg.payload)) 19 | if msg.topic == 'bathbully005': 20 | cmds = msg.payload.decode().split('#') 21 | running_mode = cmds[1] if len(cmds) > 1 else cmds[0] 22 | device = BathHeater() 23 | mappings = { 24 | 'on': '打开灯', # 打开空调 <-> 打开浴霸灯 25 | 'off': '关闭灯', # 关闭空调 <-> 关闭浴霸灯 26 | '3': '取暖', # 空调打开辅热模式 <-> 取暖 27 | '4': '换气', # 空调开启柔风模式 <-> 换气 28 | '5': '热干燥', # 空调开启干燥模式 <-> 热干燥 29 | '6': '待机', # 空调开启睡眠模式 <-> 浴霸待机 30 | '7': '凉干燥', # 空调开启节能模式 <-> 凉干燥 31 | } 32 | device.control(action=mappings[running_mode]) 33 | if msg.topic == 'aircondition005': 34 | action = msg.payload.decode() 35 | device = AirCondition() 36 | device.control(action) 37 | 38 | 39 | client = mqtt.Client(client_id=os.getenv("BEMFA_CLIENT_ID", "")) 40 | client.on_connect = on_connect 41 | client.on_message = on_message 42 | client.connect("bemfa.com", 9501, 60) 43 | client.loop_forever() 44 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | ln -s /data/www/miot-http-support/supervisor.conf /etc/supervisor/conf.d/miot-http-support.conf 2 | -------------------------------------------------------------------------------- /supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:miot-http-support] 2 | directory = /data/www/%(program_name)s 3 | command=/data/www/%(program_name)s/venv/bin/python server.py 4 | stdout_logfile_maxbytes=0 5 | stdout_logfile_backups = 0 6 | stdout_logfile = /data/logs/%(program_name)s/stdout.log 7 | stderr_logfile = /data/logs/%(program_name)s/stderr.log 8 | --------------------------------------------------------------------------------