Skip to content

Commit

Permalink
Merge pull request #505 from y-yosuke/add-switchbot-status-publisher
Browse files Browse the repository at this point in the history
Add switchbot status publisher
  • Loading branch information
k-okada committed Jul 10, 2024
2 parents e6173b8 + d0465ce commit fb32bec
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 3 deletions.
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
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

0 comments on commit fb32bec

Please sign in to comment.