Skip to content

Commit ac233c9

Browse files
committed
smart lock initial test
1 parent 8de3363 commit ac233c9

File tree

6 files changed

+229
-0
lines changed

6 files changed

+229
-0
lines changed

.env.template

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
MQTT_BROKER=
2+
MQTT_PORT=
3+
MQTT_USERNAME=
4+
MQTT_PASSWORD=
5+
MQTT_CLIENT_ID=
6+
MQTT_TOPIC_COMMAND=
7+
MQTT_TOPIC_STATUS=
8+
SWITCHBOT_TOKEN=
9+
SWITCHBOT_SECRET=
10+
SWITCHBOT_DEVICE_ID=
11+
LOG_FILE=

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Dockerfile
2+
FROM python:3.11-slim
3+
4+
WORKDIR /app
5+
6+
# Install dependencies
7+
COPY requirements.txt .
8+
RUN pip install --no-cache-dir -r requirements.txt
9+
10+
# Copy source code
11+
COPY switchbot_api2mqtt.py .
12+
COPY .env .
13+
14+
# Default command
15+
CMD ["python", "switchbot_api2mqtt.py"]

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# switchbot-api2mqtt
2+
3+
This Python script acts as a bridge between **SwitchBot's cloud APIs** (1.1 version) and the **MQTT protocol**.
4+
5+
It allows you to seamlessly integrate your SwitchBot devices, such as smart locks, into your existing smart home ecosystem that supports MQTT, like OpenHAB.
6+
7+
Designed for ease of deployment, this solution can be run natively or as a Docker container, providing a portable and consistent environment.
8+
9+
By translating commands and status updates between SwitchBot's proprietary API and standard MQTT messages, this solution enables you to control and monitor your SwitchBot devices directly from your MQTT-enabled smart home platform. This eliminates the need for separate apps or complex workarounds, providing a unified and efficient control experience.
10+
11+
## Usage
12+
13+
- Copy *.env-template* in *.env* and populate config variables according to your setup
14+
- Follow this guide to obtain your authentication secrets: https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file#getting-started
15+
- Run switchbot_api2mqtt.py using your python environment OR run with docker compose
16+
- Send commands to *MQTT_TOPIC_COMMAND* topic using an MQTT publisher
17+
- Get response and status subscribing to *MQTT_TOPIC_STATUS* topic
18+
19+
## *version 0.1*
20+
- Initial version to test the Smart Lock.
21+
- The commands are sent as simple strings in the Mqtt message
22+
- Valid commands are *lock*, *unlock*, *devices*, *status*.
23+
- Use *devices* command to obtain a list of your devices and get the DEVICE ID of your SMART LOCK
24+
- Every 60 seconds device status is automatically sent to *MQTT_TOPIC_STATUS* topic
25+
26+
## SWITCHBOT API Documentation
27+
https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file
28+
29+
# DISCLAIMER
30+
31+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37+
SOFTWARE.

docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
services:
2+
switchbot-proxy:
3+
build: .
4+
container_name: switchbot_api2mqtt
5+
restart: no
6+
env_file:
7+
- .env
8+
network_mode: host
9+
volumes:
10+
- switchbot_api2mqtt_log:/logs
11+
12+
volumes:
13+
switchbot_api2mqtt_log: null

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
paho-mqtt>=1.6.1
2+
requests
3+
python-dotenv

switchbot_api2mqtt.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# switchbot_api2mqtt.py
2+
import logging
3+
import os
4+
import json
5+
import time
6+
import uuid
7+
import hmac
8+
import hashlib
9+
import base64
10+
import requests
11+
import paho.mqtt.client as mqtt
12+
from threading import Thread
13+
from dotenv import load_dotenv
14+
15+
_VERSION = "0.1"
16+
17+
load_dotenv()
18+
log_file = os.getenv("LOG_FILE", "/logs/switchbot_api2mqtt.log")
19+
# --- Logging ---
20+
logging.basicConfig(
21+
level=logging.INFO,
22+
format="%(asctime)s [%(levelname)s] %(message)s",
23+
handlers=[
24+
logging.StreamHandler(),
25+
logging.FileHandler(log_file, encoding='utf-8')
26+
]
27+
)
28+
logger = logging.getLogger("switchbot-api2mqtt")
29+
30+
# --- Configurazioni ---
31+
MQTT_BROKER = os.getenv("MQTT_BROKER", "localhost")
32+
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
33+
MQTT_USERNAME = os.getenv("MQTT_USERNAME")
34+
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD")
35+
MQTT_CLIENT_ID = os.getenv("MQTT_CLIENT_ID", "switchbot_api2mqtt")
36+
MQTT_TOPIC_COMMAND = os.getenv("MQTT_TOPIC_COMMAND", "switchbot/lock/cmd")
37+
MQTT_TOPIC_STATUS = os.getenv("MQTT_TOPIC_STATUS", "switchbot/lock/status")
38+
SWITCHBOT_TOKEN = os.getenv("SWITCHBOT_TOKEN")
39+
SWITCHBOT_SECRET = os.getenv("SWITCHBOT_SECRET")
40+
SWITCHBOT_DEVICE_ID = os.getenv("SWITCHBOT_DEVICE_ID")
41+
42+
# --- Funzione per generare headers autenticati ---
43+
def generate_headers():
44+
t = str(int(time.time() * 1000))
45+
nonce = str(uuid.uuid4())
46+
string_to_sign = SWITCHBOT_TOKEN + t + nonce
47+
sign = base64.b64encode(hmac.new(SWITCHBOT_SECRET.encode(), msg=string_to_sign.encode(), digestmod=hashlib.sha256).digest()).decode()
48+
49+
return {
50+
"Authorization": SWITCHBOT_TOKEN,
51+
"sign": sign,
52+
"t": t,
53+
"nonce": nonce,
54+
"Content-Type": "application/json; charset=utf8"
55+
}
56+
57+
# --- Funzioni per SwitchBot ---
58+
def get_lock_status():
59+
logger.info(f"Get Status")
60+
url = f"https://api.switch-bot.com/v1.1/devices/{SWITCHBOT_DEVICE_ID}/status"
61+
headers = generate_headers()
62+
try:
63+
response = requests.get(url, headers=headers)
64+
data = response.json()
65+
if data.get("message") == "success":
66+
return data.get("body")
67+
except Exception as e:
68+
logger.error(f"Error getting status: {e}")
69+
return None
70+
71+
def get_devices():
72+
url = f"https://api.switch-bot.com/v1.1/devices"
73+
headers = generate_headers()
74+
try:
75+
response = requests.get(url, headers=headers)
76+
data = response.json()
77+
if data.get("message") == "success":
78+
return data.get("body")
79+
except Exception as e:
80+
logger.error(f"Error getting devices: {e}")
81+
return None
82+
83+
def send_command(command):
84+
url = f"https://api.switch-bot.com/v1.1/devices/{SWITCHBOT_DEVICE_ID}/commands"
85+
headers = generate_headers()
86+
payload = {
87+
"command": command,
88+
"parameter": "default",
89+
"commandType": "command"
90+
}
91+
try:
92+
response = requests.post(url, headers=headers, json=payload)
93+
logger.info(f"Response {response}")
94+
return response.status_code == 200
95+
except Exception as e:
96+
logger.error(f"Error send command: {e}")
97+
return False
98+
99+
# --- MQTT Handlers ---
100+
def on_connect(client, userdata, flags, rc, properties=None):
101+
logger.info(f"MQTT connection successfully")
102+
client.subscribe(MQTT_TOPIC_COMMAND)
103+
104+
def on_message(client, userdata, msg):
105+
command = msg.payload.decode().strip().lower()
106+
logger.info(f"Command {command}")
107+
if command in ["lock", "unlock"]:
108+
success = send_command(command)
109+
if not success:
110+
logger.error("Error executing command")
111+
elif command == "devices":
112+
res = get_devices()
113+
client.publish(MQTT_TOPIC_STATUS, json.dumps(res))
114+
elif command == "status":
115+
res = get_lock_status()
116+
client.publish(MQTT_TOPIC_STATUS, json.dumps(res))
117+
118+
# --- Thread per aggiornamento stato periodico ---
119+
def poll_status_loop(client):
120+
while True:
121+
status = get_lock_status()
122+
if status:
123+
client.publish(MQTT_TOPIC_STATUS, json.dumps(status))
124+
time.sleep(60) # ogni minuto
125+
126+
# --- Main ---
127+
if __name__ == "__main__":
128+
logger.info(f"START version {_VERSION}")
129+
mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, MQTT_CLIENT_ID)
130+
mqtt_client.on_connect = on_connect
131+
mqtt_client.on_message = on_message
132+
if MQTT_USERNAME and MQTT_PASSWORD:
133+
mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
134+
try:
135+
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
136+
except Exception as e:
137+
logger.error(f"MQTT Connection error: {e}")
138+
exit()
139+
140+
poller = Thread(target=poll_status_loop, args=(mqtt_client,), daemon=True)
141+
poller.start()
142+
143+
logger.info("Waiting for messages... CTRL+C to exit.")
144+
try:
145+
mqtt_client.loop_forever()
146+
except KeyboardInterrupt:
147+
logger.error("Subscriber disconnected.")
148+
finally:
149+
mqtt_client.disconnect()
150+
logger.error("Subscriber disconnected.")

0 commit comments

Comments
 (0)