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

Add switchbot status publisher #505

Merged
merged 9 commits into from
Jul 10, 2024
5 changes: 5 additions & 0 deletions switchbot_ros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ add_message_files(
FILES
Device.msg
DeviceArray.msg
Meter.msg
PlugMini.msg
Hub2.msg
Bot.msg
StripLight.msg
)

add_action_files(
Expand Down
15 changes: 15 additions & 0 deletions switchbot_ros/launch/switchbot.launch
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<arg name="token" />
<arg name="secret" default="''" />
<arg name="respawn" default="true" />
<arg name="pub_status" default="false" />
<arg name="pub_status_rate" default="0.1" />
<arg name="pub_device_name" default="bot74a" />

<node name="switchbot_ros" pkg="switchbot_ros" type="switchbot_ros_server.py"
respawn="$(arg respawn)" output="screen">
Expand All @@ -10,4 +13,16 @@
secret: $(arg secret)
</rosparam>
</node>

<node if="$(arg pub_status)"
name="switchbot_status_publisher" pkg="switchbot_ros" type="switchbot_status_publisher.py"
respawn="$(arg respawn)" output="screen">
<rosparam subst_value="true">
token: $(arg token)
secret: $(arg secret)
device_name: $(arg pub_device_name)
rate: $(arg pub_status_rate)
</rosparam>
</node>

</launch>
10 changes: 10 additions & 0 deletions switchbot_ros/msg/Bot.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
string DEVICEMODE_PRESS = "pressMode"
string DEVICEMODE_SWITCH = "switchMode"
string DEVICEMODE_CUSTOMIZE = "customizeMode"

Header header # timestamp

float64 battery # the current battery level, 0-100

bool power # ON/OFF state True/False
string device_mode # pressMode, switchMode, or customizeMode
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add candidates constants of this field to this message like https://docs.ros.org/en/noetic/api/visualization_msgs/html/msg/Marker.html ?

string DEVICEMODE_PRESS = "pressMode"

or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added 3 candedate constants as

string DEVICEMODE_PRESS     = "pressMode"
string DEVICEMODE_SWITCH    = "switchMode"
string DEVICEMODE_CUSTOMIZE = "customizeMode"

6 changes: 6 additions & 0 deletions switchbot_ros/msg/Hub2.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Header header # timestamp

float64 temperature # temperature in celsius
float64 humidity # humidity percentage

int64 light_level # the level of illuminance of the ambience light, 1~20
5 changes: 5 additions & 0 deletions switchbot_ros/msg/Meter.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Header header # timestamp

float64 temperature # temperature in celsius
float64 humidity # humidity percentage
float64 battery # the current battery level, 0-100
7 changes: 7 additions & 0 deletions switchbot_ros/msg/PlugMini.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Header header # timestamp

float64 voltage # the voltage of the device, measured in Volt
float64 weight # the power consumed in a day, measured in Watts
float64 current # the current of the device at the moment, measured in Amp

int32 minutes_day # he duration that the device has been used during a day, measured in minutes
8 changes: 8 additions & 0 deletions switchbot_ros/msg/StripLight.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Header header # timestamp

bool power # ON/OFF state True/False

int64 brightness # the brightness value, range from 1 to 100
int64 color_r # Red color value 0-255
int64 color_g # Green color value 0-255
int64 color_b # Blue color value 0-255
4 changes: 2 additions & 2 deletions switchbot_ros/scripts/control_switchbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
devices = client.get_devices()
print(devices)

client.control_device('pendant-light', 'turnOff')
client.control_device('pendant-light', 'turnOn', wait=True)

client.control_device('bot74a', 'turnOn')
client.control_device('bot74a', 'turnOn', wait=True)

193 changes: 193 additions & 0 deletions switchbot_ros/scripts/switchbot_status_publisher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#!/usr/bin/env python

import os.path
from requests import ConnectionError
import rospy
from switchbot_ros.switchbot import SwitchBotAPIClient
from switchbot_ros.switchbot import DeviceError, SwitchBotAPIError
from switchbot_ros.msg import Meter, PlugMini, Hub2, Bot, StripLight


class SwitchBotStatusPublisher:
"""
Publissh your switchbot status with ROS and SwitchBot API
"""
def __init__(self):
# SwitchBot configs
# '~token' can be file path or raw characters
token = rospy.get_param('~token')
if os.path.exists(token):
with open(token) as f:
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()

# Get parameters for publishing
self.rate = rospy.get_param('~rate', 0.1)
rospy.loginfo('Rate: ' + str(self.rate))

device_name = rospy.get_param('~device_name')
if device_name:
self.device_name = device_name
else:
rospy.logerr('No Device Name')
return

self.device_type = None
self.device_list = sorted(
self.bots.device_list,
key=lambda device: str(device.get('deviceName')))
for device in self.device_list:
device_name = str(device.get('deviceName'))
if self.device_name == device_name:
self.device_type = str(device.get('deviceType'))

if self.device_type:
rospy.loginfo('deviceName: ' + self.device_name + ' / deviceType: ' + self.device_type)
else:
rospy.logerr('Invalid Device Name: ' + self.device_name)
return

topic_name = '~' + self.device_name
topic_name = topic_name.replace('-', '_')

# Publisher Message Class for each device type
if self.device_type == 'Remote':
rospy.logerr('Device Type: "' + self.device_type + '" has no status in specifications.')
return
else:
if self.device_type == 'Meter':
self.msg_class = Meter
elif self.device_type == 'MeterPlus':
self.msg_class = Meter
elif self.device_type == 'WoIOSensor':
self.msg_class = Meter
elif self.device_type == 'Hub 2':
self.msg_class = Hub2
elif self.device_type == 'Plug Mini (JP)':
self.msg_class = PlugMini
elif self.device_type == 'Plug Mini (US)':
self.msg_class = PlugMini
elif self.device_type == 'Bot':
self.msg_class = Bot
elif self.device_type == 'Strip Light':
self.msg_class = StripLight
else:
rospy.logerr('No publisher process for "' + self.device_type + '" in switchbot_status_publisher.py')
return

self.status_pub = rospy.Publisher(topic_name, self.msg_class, queue_size=1, latch=True)

rospy.loginfo('Ready: SwitchBot Status Publisher for ' + self.device_name)


def get_switchbot_client(self):
try:
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
rospy.logwarn_once('Failed to connect to the switchbot server. The client would try connecting to it when subscribes the ActionGoal topic.')
return None


def spin(self):
rate = rospy.Rate(self.rate)
while not rospy.is_shutdown():
rate.sleep()
if self.bots is None:
self.bots = self.get_switchbot_client()

if self.device_type == 'Remote':
return
else:
status = self.get_device_status(device_name=self.device_name)

if status:
time = rospy.get_rostime()
if self.msg_class == Meter:
msg = Meter()
msg.header.stamp = time
msg.temperature = status['temperature']
msg.humidity = status['humidity']
msg.battery = status['battery']
elif self.msg_class == Hub2:
msg = Hub2()
msg.header.stamp = time
msg.temperature = status['temperature']
msg.humidity = status['humidity']
msg.light_level = status['lightLevel']
elif self.msg_class == PlugMini:
msg = PlugMini()
msg.header.stamp = time
msg.voltage = status['voltage']
msg.weight = status['weight']
msg.current = status['electricCurrent']
msg.minutes_day = status['electricityOfDay']
elif self.msg_class == Bot:
msg = Bot()
msg.header.stamp = time
msg.battery = status['battery']
if status['power'] == 'on':
msg.power = True
else:
msg.power = False
msg.device_mode = status['deviceMode']
elif self.msg_class == StripLight:
msg = StripLight()
msg.header.stamp = time
if status['power'] == 'on':
msg.power = True
else:
msg.power = False
msg.brightness = status['brightness']
rgb_string = status['color']
r, g, b = map(int, rgb_string.split(':'))
msg.color_r = r
msg.color_g = g
msg.color_b = b
else:
return

if msg:
self.status_pub.publish(msg)


def get_device_status(self, device_name=None):
if self.bots is None:
return
elif device_name:
status = self.bots.device_status(device_name=device_name)
return status
else:
return


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)


if __name__ == '__main__':
try:
rospy.init_node('switchbot_status_publisher')
ssp = SwitchBotStatusPublisher()
ssp.spin()
except rospy.ROSInterruptException:
pass
5 changes: 4 additions & 1 deletion switchbot_ros/src/switchbot_ros/switchbot_ros_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ class SwitchBotROSClient(object):

def __init__(self,
actionname='switchbot_ros/switch',
topicname='switchbot_ros/devices'):
topicname='switchbot_ros/devices',
timeout=5):

self.actionname = actionname
self.topicname = topicname
self.action_client = actionlib.SimpleActionClient(
actionname,
SwitchBotCommandAction
)
rospy.loginfo("Waiting for action server to start. (timeout: " + str(timeout) + "[sec])")
self.action_client.wait_for_server(timeout=rospy.Duration(timeout,0))

def get_devices(self, timeout=None):

Expand Down
Loading