Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mod for switchbot api 1.1 #491

Merged
merged 7 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions switchbot_ros/launch/switchbot.launch
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<launch>
<arg name="token" />
<arg name="secret" default="''" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use null and it will be treated as None in python.

Suggested change
<arg name="secret" default="''" />
<arg name="secret" default="null"/>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my environments, both <arg name="secret" default="null" /> and <arg name="secret" default="" /> make an error load_parameters: unable to set parameters (last param was [/switchbot_ros/secret=None]): cannot marshal None unless allow_none is enabled and quit the process. But <arg name="secret" default="''" /> works finely.

  • My Environments
    • Ubuntu 20.04.6 LTS - ROS Noetic - Python 3.8.1
    • Ubuntu 18.04.6 LTS - ROS Melodic - Python 2.7.17

Does this problem depend on just only my environments?
Or do you know any solutions for the load_parameter error other than <arg name="secret" default="''" />?


The console output with load_parameter error occurs when I used <arg name="secret" default="null" /> and <arg name="secret" default="" /> in switchbot.launch is shown as followings.

robotuser@robotuser-PC:~/switchbot_ws$ roslaunch switchbot_ros switchbot.launch token:='(省略)' --screen
... logging to /home/robotuser/.ros/log/fc70881c-34c9-11ee-84c2-43599b20a8be/roslaunch-robotuser-PC-68523.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://robotuser-PC:34991/

SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.0
 * /switchbot_ros/secret: None
 * /switchbot_ros/token: ca6f439a05b820c78...

NODES
  /
    switchbot_ros (switchbot_ros/switchbot_ros_server.py)

auto-starting new master
process[master]: started with pid [68531]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to fc70881c-34c9-11ee-84c2-43599b20a8be
process[rosout-1]: started with pid [68541]
started core service [/rosout]
load_parameters: unable to set parameters (last param was [/switchbot_ros/secret=None]): cannot marshal None unless allow_none is enabled
Traceback (most recent call last):
  File "/opt/ros/noetic/lib/python3/dist-packages/roslaunch/__init__.py", line 347, in main
    p.start()
  File "/opt/ros/noetic/lib/python3/dist-packages/roslaunch/parent.py", line 316, in start
    self.runner.launch()
  File "/opt/ros/noetic/lib/python3/dist-packages/roslaunch/launch.py", line 676, in launch
    self._setup()
  File "/opt/ros/noetic/lib/python3/dist-packages/roslaunch/launch.py", line 663, in _setup
    self._load_parameters()
  File "/opt/ros/noetic/lib/python3/dist-packages/roslaunch/launch.py", line 354, in _load_parameters
    r  = param_server_multi()
  File "/usr/lib/python3.8/xmlrpc/client.py", line 879, in __call__
    return MultiCallIterator(self.__server.system.multicall(marshalled_list))
  File "/usr/lib/python3.8/xmlrpc/client.py", line 1109, in __call__
    return self.__send(self.__name, args)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 1447, in __request
    request = dumps(params, methodname, encoding=self.__encoding,
  File "/usr/lib/python3.8/xmlrpc/client.py", line 968, in dumps
    data = m.dumps(params)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 501, in dumps
    dump(v, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 523, in __dump
    f(self, value, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 576, in dump_array
    dump(v, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 523, in __dump
    f(self, value, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 594, in dump_struct
    dump(v, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 523, in __dump
    f(self, value, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 576, in dump_array
    dump(v, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 523, in __dump
    f(self, value, write)
  File "/usr/lib/python3.8/xmlrpc/client.py", line 527, in dump_nil
    raise TypeError("cannot marshal None unless allow_none is enabled")
TypeError: cannot marshal None unless allow_none is enabled
[rosout-1] killing on exit
[master] killing on exit
robotuser@robotuser-PC:~/switchbot_ws$ ^C

<arg name="respawn" default="true" />

<node name="switchbot_ros" pkg="switchbot_ros" type="switchbot_ros_server.py"
respawn="$(arg respawn)" output="screen">
<rosparam subst_value="true">
token: $(arg token)
secret: $(arg secret)
</rosparam>
</node>
</launch>
15 changes: 15 additions & 0 deletions switchbot_ros/scripts/control_switchbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python

import rospy
from switchbot_ros.switchbot_ros_client import SwitchBotROSClient

rospy.init_node('controler_node')
client = SwitchBotROSClient()

devices = client.get_devices()
print(devices)

client.control_device('pendant-light', 'turnOff')

client.control_device('bot74a', 'turnOn')

83 changes: 74 additions & 9 deletions switchbot_ros/scripts/switchbot_ros_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,20 @@ def __init__(self):
self.token = f.read().replace('\n', '')
else:
self.token = token

# Switchbot API v1.1 needs secret key
secret = rospy.get_param('~secret', None )
if secret is not None and os.path.exists(secret):
with open(secret, 'r', encoding='utf-8') as f:
self.secret = f.read().replace('\n', '')
else:
self.secret = secret

# Initialize switchbot client
self.bots = self.get_switchbot_client()
self.print_apiversion()
self.print_devices()
self.print_scenes()
# Actionlib
self._as = actionlib.SimpleActionServer(
'~switch', SwitchBotCommandAction,
Expand All @@ -37,10 +48,11 @@ def __init__(self):
# Topic
self.pub = rospy.Publisher('~devices', DeviceArray, queue_size=1, latch=True)
self.published = False
rospy.loginfo('Ready.')

def get_switchbot_client(self):
try:
client = SwitchBotAPIClient(token=self.token)
client = SwitchBotAPIClient(token=self.token, secret=self.secret)
rospy.loginfo('Switchbot API Client initialized.')
return client
except ConnectionError: # If the machine is not connected to the internet
Expand All @@ -57,31 +69,84 @@ def spin(self):
self.publish_devices()
self.published = True

def print_apiversion(self):
if self.bots is None:
return

apiversion_str = 'Using SwitchBot API ';
apiversion_str += self.bots.api_version;
rospy.loginfo(apiversion_str)


def print_devices(self):
if self.bots is None:
return
device_list_str = 'Switchbot device list:\n'

device_list_str = 'Switchbot Device List:\n'
device_list = sorted(
self.bots.device_list,
key=lambda device: str(device['deviceName']))
key=lambda device: str(device.get('deviceName')))
device_list_str += str(len(device_list)) + ' Item(s)\n'
for device in device_list:
device_list_str += 'Name: ' + str(device['deviceName'])
device_list_str += ', Type: ' + str(device['deviceType'])
device_list_str += 'deviceName: ' + str(device.get('deviceName'))
device_list_str += ', deviceID: ' + str(device.get('deviceId'))
device_list_str += ', deviceType: ' + str(device.get('deviceType'))
device_list_str += '\n'
rospy.loginfo(device_list_str)

remote_list_str = 'Switchbot Remote List:\n'
infrared_remote_list = sorted(
self.bots.infrared_remote_list,
key=lambda infrared_remote: str(infrared_remote.get('deviceName')))
remote_list_str += str(len(infrared_remote_list)) + ' Item(s)\n'
for infrared_remote in infrared_remote_list:
remote_list_str += 'deviceName: ' + str(infrared_remote.get('deviceName'))
remote_list_str += ', deviceID: ' + str(infrared_remote.get('deviceId'))
remote_list_str += ', remoteType: ' + str(infrared_remote.get('remoteType'))
remote_list_str += '\n'
rospy.loginfo(remote_list_str)


def print_scenes(self):
if self.bots is None:
return

scene_list_str = 'Switchbot Scene List:\n'
scene_list = sorted(
self.bots.scene_list,
key=lambda scene: str(scene.get('sceneName')))
scene_list_str += str(len(scene_list)) + ' Item(s)\n'
for scene in scene_list:
scene_list_str += 'sceneName: ' + str(scene.get('sceneName'))
scene_list_str += ', sceneID: ' + str(scene.get('sceneId'))
scene_list_str += '\n'
rospy.loginfo(scene_list_str)


def publish_devices(self):
if self.bots is None:
return

msg = DeviceArray()

device_list = sorted(
self.bots.device_list,
key=lambda device: str(device['deviceName']))
key=lambda device: str(device.get('deviceName')))
for device in device_list:
msg_device = Device()
msg_device.name = str(device['deviceName'])
msg_device.type = str(device['deviceType'])
msg_device.name = str(device.get('deviceName'))
msg_device.type = str(device.get('deviceType'))
msg.devices.append(msg_device)

infrared_remote_list = sorted(
self.bots.infrared_remote_list,
key=lambda infrared_remote: str(infrared_remote.get('deviceName')))
for infrared_remote in infrared_remote_list:
msg_device = Device()
msg_device.name = str(infrared_remote.get('deviceName'))
msg_device.type = str(infrared_remote.get('remoteType'))
msg.devices.append(msg_device)

self.pub.publish(msg)

def execute_cb(self, goal):
Expand All @@ -97,7 +162,7 @@ def execute_cb(self, goal):
command_type = 'command'
try:
if not self.bots:
self.bots = SwitchBotAPIClient(token=self.token)
self.bots = SwitchBotAPIClient(token=self.token, secret=self.secret)
feedback.status = str(
self.bots.control_device(
command=goal.command,
Expand Down
80 changes: 64 additions & 16 deletions switchbot_ros/src/switchbot_ros/switchbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@
import os.path
import requests

import sys
import os
import time
import hashlib
import hmac
import base64
import uuid


class SwitchBotAPIClient(object):
"""
For Using SwitchBot via official API.
Please see https://github.com/OpenWonderLabs/SwitchBotAPI for details.
"""
def __init__(self, token):
self._host_domain = "https://api.switch-bot.com/v1.0/"
def __init__(self, token, secret=""):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, token, secret=""):
def __init__(self, token, secret=None):

if not secret:
self.api_version = "v1.0"
else:
self.api_version = "v1.1"
self._host_domain = "https://api.switch-bot.com/" + self.api_version + "/"
self.token = token
self.secret = secret # SwitchBot API v1.1
self.device_list = None
self.infrared_remote_list = None
self.scene_list = None
Expand All @@ -21,6 +34,41 @@ def __init__(self, token):
self.update_device_list()
self.update_scene_list()

def make_sign(self, token, secret):
"""
Make Sign from token and secret
"""
nonce = uuid.uuid4()
t = int(round(time.time() * 1000))
string_to_sign = '{}{}{}'.format(token, t, nonce)

if sys.version_info[0] > 2:
string_to_sign = bytes(string_to_sign, 'utf-8')
secret = bytes(secret, 'utf-8')
else:
string_to_sign = bytes(string_to_sign)
secret = bytes(secret)

sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())

if sys.version_info[0] > 2:
sign = sign.decode('utf-8')

return sign, str(t), nonce

def make_request_header(self, token, secret):
"""
Make Request Header
"""
sign, t, nonce = self.make_sign(token, secret)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep backward compatibility for v1.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if a request is sent to SwitchBot API v1.0 with the secret key included in the request header, it behaves like it is just not used and the request seems executed without any problems.

Even so, are there any compatibility issues with API V1.0?
I'm not familiar with Web API, so any advice would be appreciated.


As for another problem about Python 2 compatibility, I changed the code to work in both Python 2 and 3.

429cd88#diff-396497c868baf8630a5e57ccf98059c7d6f1d45e91faccfe0917e1012f124191R45-R66

headers={
"Authorization": token,
"sign": str(sign),
"t": str(t),
"nonce": str(nonce),
"Content-Type": "application/json; charset=utf8"
}
return headers

def request(self, method='GET', devices_or_scenes='devices', service_id='', service='', json_body=None):
"""
Expand All @@ -30,20 +78,20 @@ def request(self, method='GET', devices_or_scenes='devices', service_id='', serv
raise ValueError('Please set devices_or_scenes variable devices or scenes')

url = os.path.join(self._host_domain, devices_or_scenes, service_id, service)

headers = self.make_request_header(self.token, self.secret)

if method == 'GET':
response = requests.get(
url,
headers={'Authorization': self.token}
headers=headers
)
elif method == 'POST':
response = requests.post(
url,
json_body,
headers={
'Content-Type': 'application/json; charset=utf8',
'Authorization': self.token
})
json=json_body,
headers=headers
)
else:
raise ValueError('Got unexpected http request method. Please use GET or POST.')

Expand Down Expand Up @@ -87,8 +135,8 @@ def update_device_list(self):
self.infrared_remote_list = res['body']['infraredRemoteList']
for device in self.device_list:
self.device_name_id[device['deviceName']] = device['deviceId']
for infrated_remote in self.infrared_remote_list:
self.device_name_id[device['deviceName']] = device['deviceId']
for infrared_remote in self.infrared_remote_list:
self.device_name_id[infrared_remote['deviceName']] = infrared_remote['deviceId']

return self.device_list, self.infrared_remote_list

Expand All @@ -99,7 +147,7 @@ def update_scene_list(self):
"""
self.scene_list = self.request(devices_or_scenes='scenes')['body']
for scene in self.scene_list:
self.scene_name_id[scene['sceneName']] = device['sceneId']
self.scene_name_id[scene['sceneName']] = scene['sceneId']

return self.scene_list

Expand All @@ -125,11 +173,11 @@ def control_device(self, command, parameter='default', command_type='command', d
"""
Send Command to the device. Please see https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands for command options.
"""
json_body = json.dumps({
"command": command,
"parameter": parameter,
"commandType": command_type
})
json_body = {
"command": str(command),
"parameter": str(parameter),
"commandType": str(command_type)
}
if device_id:
pass
elif device_name:
Expand Down
Loading