diff --git a/switchbot_ros/CMakeLists.txt b/switchbot_ros/CMakeLists.txt index 20160ff79..cac584f7d 100644 --- a/switchbot_ros/CMakeLists.txt +++ b/switchbot_ros/CMakeLists.txt @@ -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( diff --git a/switchbot_ros/launch/switchbot.launch b/switchbot_ros/launch/switchbot.launch index 7e5a69e0b..cb9a68a96 100644 --- a/switchbot_ros/launch/switchbot.launch +++ b/switchbot_ros/launch/switchbot.launch @@ -2,6 +2,9 @@ + + + @@ -10,4 +13,16 @@ secret: $(arg secret) + + + + token: $(arg token) + secret: $(arg secret) + device_name: $(arg pub_device_name) + rate: $(arg pub_status_rate) + + + diff --git a/switchbot_ros/msg/Bot.msg b/switchbot_ros/msg/Bot.msg new file mode 100644 index 000000000..74955d462 --- /dev/null +++ b/switchbot_ros/msg/Bot.msg @@ -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 diff --git a/switchbot_ros/msg/Hub2.msg b/switchbot_ros/msg/Hub2.msg new file mode 100644 index 000000000..dfdf40b4f --- /dev/null +++ b/switchbot_ros/msg/Hub2.msg @@ -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 diff --git a/switchbot_ros/msg/Meter.msg b/switchbot_ros/msg/Meter.msg new file mode 100644 index 000000000..df731f552 --- /dev/null +++ b/switchbot_ros/msg/Meter.msg @@ -0,0 +1,5 @@ +Header header # timestamp + +float64 temperature # temperature in celsius +float64 humidity # humidity percentage +float64 battery # the current battery level, 0-100 diff --git a/switchbot_ros/msg/PlugMini.msg b/switchbot_ros/msg/PlugMini.msg new file mode 100644 index 000000000..e74b320d0 --- /dev/null +++ b/switchbot_ros/msg/PlugMini.msg @@ -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 diff --git a/switchbot_ros/msg/StripLight.msg b/switchbot_ros/msg/StripLight.msg new file mode 100644 index 000000000..4164d184e --- /dev/null +++ b/switchbot_ros/msg/StripLight.msg @@ -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 \ No newline at end of file diff --git a/switchbot_ros/scripts/control_switchbot.py b/switchbot_ros/scripts/control_switchbot.py index 1c04c2b73..3dede636e 100644 --- a/switchbot_ros/scripts/control_switchbot.py +++ b/switchbot_ros/scripts/control_switchbot.py @@ -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) diff --git a/switchbot_ros/scripts/switchbot_status_publisher.py b/switchbot_ros/scripts/switchbot_status_publisher.py new file mode 100644 index 000000000..6ed571728 --- /dev/null +++ b/switchbot_ros/scripts/switchbot_status_publisher.py @@ -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 diff --git a/switchbot_ros/src/switchbot_ros/switchbot_ros_client.py b/switchbot_ros/src/switchbot_ros/switchbot_ros_client.py index cd1b8a8d8..df1a7b00a 100644 --- a/switchbot_ros/src/switchbot_ros/switchbot_ros_client.py +++ b/switchbot_ros/src/switchbot_ros/switchbot_ros_client.py @@ -9,7 +9,8 @@ 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 @@ -17,6 +18,8 @@ def __init__(self, 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):