Added all missing file (.git issue)

This commit is contained in:
ThomasFransolet 2019-06-20 22:03:01 +02:00
parent e55358c438
commit 521b27505e
145 changed files with 27817 additions and 4 deletions

@ -1 +0,0 @@
Subproject commit cb649680218eada1c7b1fcca4b1bfd6f492ae5c6

View File

@ -0,0 +1,23 @@
from spidev import SpiDev
class MCP3008:
def __init__(self, bus = 0, device = 0):
self.bus, self.device = bus, device
self.spi = SpiDev()
self.open()
self.spi.max_speed_hz = 1000000 # 1MHz
def open(self):
self.spi.open(self.bus, self.device)
self.spi.max_speed_hz = 1000000 # 1MHz
def read(self, channel = 0):
cmd1 = 4 | 2 | (( channel & 4) >> 2)
cmd2 = (channel & 3) << 6
adc = self.spi.xfer2([cmd1, cmd2, 0])
data = ((adc[1] & 15) << 8) + adc[2]
return data
def close(self):
self.spi.close()

Binary file not shown.

View File

@ -0,0 +1,6 @@
Raspberry Pi Gas Sensor MQ Python Example
================
Tutorial (german): https://tutorials-raspberrypi.de/raspberry-pi-gas-sensor-mq2-konfigurieren-und-auslesen
Tutorial (english): https://tutorials-raspberrypi.com/configure-and-read-out-the-raspberry-pi-gas-sensor-mq-x

View File

@ -0,0 +1,17 @@
from mq import *
import sys, time
try:
print("Press CTRL+C to abort.")
mq = MQ();
while True:
perc = mq.MQPercentage()
sys.stdout.write("\r")
sys.stdout.write("\033[K")
sys.stdout.write("LPG: %g ppm, CO: %g ppm, Smoke: %g ppm" % (perc["GAS_LPG"], perc["CO"], perc["SMOKE"]))
sys.stdout.flush()
time.sleep(0.1)
except:
print("\nAbort by user")

View File

@ -0,0 +1,140 @@
# adapted from sandboxelectronics.com/?p=165
import time
import math
from MCP3008 import MCP3008
class MQ():
######################### Hardware Related Macros #########################
MQ_PIN = 0 # define which analog input channel you are going to use (MCP3008)
RL_VALUE = 5 # define the load resistance on the board, in kilo ohms
RO_CLEAN_AIR_FACTOR = 9.83 # RO_CLEAR_AIR_FACTOR=(Sensor resistance in clean air)/RO,
# which is derived from the chart in datasheet
######################### Software Related Macros #########################
CALIBARAION_SAMPLE_TIMES = 50 # define how many samples you are going to take in the calibration phase
CALIBRATION_SAMPLE_INTERVAL = 500 # define the time interal(in milisecond) between each samples in the
# cablibration phase
READ_SAMPLE_INTERVAL = 50 # define how many samples you are going to take in normal operation
READ_SAMPLE_TIMES = 5 # define the time interal(in milisecond) between each samples in
# normal operation
######################### Application Related Macros ######################
GAS_LPG = 0
GAS_CO = 1
GAS_SMOKE = 2
def __init__(self, Ro=10, analogPin=0):
self.Ro = Ro
self.MQ_PIN = analogPin
self.adc = MCP3008()
self.LPGCurve = [2.3,0.21,-0.47] # two points are taken from the curve.
# with these two points, a line is formed which is "approximately equivalent"
# to the original curve.
# data format:{ x, y, slope}; point1: (lg200, 0.21), point2: (lg10000, -0.59)
self.COCurve = [2.3,0.72,-0.34] # two points are taken from the curve.
# with these two points, a line is formed which is "approximately equivalent"
# to the original curve.
# data format:[ x, y, slope]; point1: (lg200, 0.72), point2: (lg10000, 0.15)
self.SmokeCurve =[2.3,0.53,-0.44] # two points are taken from the curve.
# with these two points, a line is formed which is "approximately equivalent"
# to the original curve.
# data format:[ x, y, slope]; point1: (lg200, 0.53), point2: (lg10000, -0.22)
print("Calibrating...")
self.Ro = self.MQCalibration(self.MQ_PIN)
print("Calibration is done...\n")
print("Ro=%f kohm" % self.Ro)
def MQPercentage(self):
val = {}
read = self.MQRead(self.MQ_PIN)
val["GAS_LPG"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_LPG)
val["CO"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_CO)
val["SMOKE"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_SMOKE)
return val
######################### MQResistanceCalculation #########################
# Input: raw_adc - raw value read from adc, which represents the voltage
# Output: the calculated sensor resistance
# Remarks: The sensor and the load resistor forms a voltage divider. Given the voltage
# across the load resistor and its resistance, the resistance of the sensor
# could be derived.
############################################################################
def MQResistanceCalculation(self, raw_adc):
return float(self.RL_VALUE*(1023.0-raw_adc)/float(raw_adc));
######################### MQCalibration ####################################
# Input: mq_pin - analog channel
# Output: Ro of the sensor
# Remarks: This function assumes that the sensor is in clean air. It use
# MQResistanceCalculation to calculates the sensor resistance in clean air
# and then divides it with RO_CLEAN_AIR_FACTOR. RO_CLEAN_AIR_FACTOR is about
# 10, which differs slightly between different sensors.
############################################################################
def MQCalibration(self, mq_pin):
val = 0.0
for i in range(self.CALIBARAION_SAMPLE_TIMES): # take multiple samples
val += self.MQResistanceCalculation(self.adc.read(mq_pin))
time.sleep(self.CALIBRATION_SAMPLE_INTERVAL/1000.0)
val = val/self.CALIBARAION_SAMPLE_TIMES # calculate the average value
val = val/self.RO_CLEAN_AIR_FACTOR # divided by RO_CLEAN_AIR_FACTOR yields the Ro
# according to the chart in the datasheet
return val;
######################### MQRead ##########################################
# Input: mq_pin - analog channel
# Output: Rs of the sensor
# Remarks: This function use MQResistanceCalculation to caculate the sensor resistenc (Rs).
# The Rs changes as the sensor is in the different consentration of the target
# gas. The sample times and the time interval between samples could be configured
# by changing the definition of the macros.
############################################################################
def MQRead(self, mq_pin):
rs = 0.0
for i in range(self.READ_SAMPLE_TIMES):
rs += self.MQResistanceCalculation(self.adc.read(mq_pin))
time.sleep(self.READ_SAMPLE_INTERVAL/1000.0)
rs = rs/self.READ_SAMPLE_TIMES
return rs
######################### MQGetGasPercentage ##############################
# Input: rs_ro_ratio - Rs divided by Ro
# gas_id - target gas type
# Output: ppm of the target gas
# Remarks: This function passes different curves to the MQGetPercentage function which
# calculates the ppm (parts per million) of the target gas.
############################################################################
def MQGetGasPercentage(self, rs_ro_ratio, gas_id):
if ( gas_id == self.GAS_LPG ):
return self.MQGetPercentage(rs_ro_ratio, self.LPGCurve)
elif ( gas_id == self.GAS_CO ):
return self.MQGetPercentage(rs_ro_ratio, self.COCurve)
elif ( gas_id == self.GAS_SMOKE ):
return self.MQGetPercentage(rs_ro_ratio, self.SmokeCurve)
return 0
######################### MQGetPercentage #################################
# Input: rs_ro_ratio - Rs divided by Ro
# pcurve - pointer to the curve of the target gas
# Output: ppm of the target gas
# Remarks: By using the slope and a point of the line. The x(logarithmic value of ppm)
# of the line could be derived if y(rs_ro_ratio) is provided. As it is a
# logarithmic coordinate, power of 10 is used to convert the result to non-logarithmic
# value.
############################################################################
def MQGetPercentage(self, rs_ro_ratio, pcurve):
return (math.pow(10,( ((math.log(rs_ro_ratio)-pcurve[1])/ pcurve[2]) + pcurve[0])))

Binary file not shown.

@ -1 +0,0 @@
Subproject commit 34f9422f89605c0994ae718d8ec42322ddaec996

110
RPI Code/Meross_/MerossIot/.gitignore vendored Normal file
View File

@ -0,0 +1,110 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Local test
local_test/
# IntelliJ
.idea

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Alberto Geniola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,102 @@
[![Build status](https://albertogeniola.visualstudio.com/Meross/_apis/build/status/Meross-Python%20package-CI)](https://albertogeniola.visualstudio.com/Meross/_build/latest?definitionId=1)
![Deployment](https://albertogeniola.vsrm.visualstudio.com/_apis/public/Release/badge/c4128d1b-c23c-418d-95c5-2de061954ee5/1/1)
# Meross IoT library
A pure-python based library providing API for controlling Meross IoT devices over the internet.
To see what devices are currently supported, checkout the *Currently supported devices* section.
Hopefully, more Meross hardware will be supported in the future.
This library is still work in progress, therefore use it with caution.
## Installation
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
So, the installation is as simple as typing the following command:
```
pip install meross_iot --upgrade
```
## Usage
The following script demonstrates how to use this library.
```python
import time
import sys
from meross_iot.api import MerossHttpClient
if __name__=='__main__':
httpHandler = MerossHttpClient(email="YOUR_MEROSS_CLOUD_EMAIL", password="YOUR_PASSWORD")
# Retrieves the list of supported devices
print("Listing Devices...")
devices = httpHandler.list_supported_devices()
for counter, device in enumerate(devices):
print("Playing with device: %d" % counter)
# Returns most of the info about the power plug
print("\nGetting system data...")
data = device.get_sys_data()
# Turns the power-plug on
print("\nTurning the device on...")
device.turn_off()
# Turns the power-plug off
print("\nTurning the device off...")
device.turn_on()
# Reads the historical device consumption
print("\nReading consumption data...")
consumption = device.get_power_consumptionX()
# Returns the list of WIFI Network available for the plug
# (Note. this takes some time to complete)
print("\nScanning Wifi...")
wifi_list = device.get_wifi_list()
# Info about the device
print("\nGetting device trace...")
trace = device.get_trace()
print("\nGetting device debug...")
debug = device.get_debug()
# Returns the capabilities of this device
print("\nRetrieving device abilities...")
abilities = device.get_abilities()
# I still have to figure this out :S
# The following command is not yet implemented on all devices
# and might not work as expected.
# report = device.get_report()
# Returns the current power consumption and voltage from the plug
# (Note: this is not really realtime, but close enough)
print("\nReading electricity...")
electricity = device.get_electricity()
```
## Currently supported devices
Even though this library was firstly meant to drive only the Meross MSS310,
other nice developers contributed to its realization. The following is the
currently supported list of devices:
- MSS310 both hw v1 and v2 (Thanks to [DanoneKiD](https://github.com/DanoneKiD))
- MSS210 (Thanks to [ictes](https://github.com/ictes))
- MSS110 (Thanks to [soberstadt](https://github.com/soberstadt))
- MSS425E (Thanks to [ping-localhost](https://github.com/ping-localhost))
## Protocol details
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
## Donate!
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy. However, if you liked or appreciated by work, why don't you buy me a beer? It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
[![Buy me a beer](http://4.bp.blogspot.com/-1Md6-deTZ84/VA_lzcxMx1I/AAAAAAAACl8/wP_4rXBXwyI/s1600/PayPal-Donation-Button.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6HPAB89UYSZF2)

View File

@ -0,0 +1,102 @@
[![Build status](https://albertogeniola.visualstudio.com/Meross/_apis/build/status/Meross-Python%20package-CI)](https://albertogeniola.visualstudio.com/Meross/_build/latest?definitionId=1)
![Deployment](https://albertogeniola.vsrm.visualstudio.com/_apis/public/Release/badge/c4128d1b-c23c-418d-95c5-2de061954ee5/1/1)
# Meross IoT library
A pure-python based library providing API for controlling Meross IoT devices over the internet.
To see what devices are currently supported, checkout the *Currently supported devices* section.
Hopefully, more Meross hardware will be supported in the future.
This library is still work in progress, therefore use it with caution.
## Installation
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
So, the installation is as simple as typing the following command:
```
pip install meross_iot --upgrade
```
## Usage
The following script demonstrates how to use this library.
```python
import time
import sys
from meross_iot.api import MerossHttpClient
if __name__=='__main__':
httpHandler = MerossHttpClient(email="YOUR_MEROSS_CLOUD_EMAIL", password="YOUR_PASSWORD")
# Retrieves the list of supported devices
print("Listing Devices...")
devices = httpHandler.list_supported_devices()
for counter, device in enumerate(devices):
print("Playing with device: %d" % counter)
# Returns most of the info about the power plug
print("\nGetting system data...")
data = device.get_sys_data()
# Turns the power-plug on
print("\nTurning the device on...")
device.turn_off()
# Turns the power-plug off
print("\nTurning the device off...")
device.turn_on()
# Reads the historical device consumption
print("\nReading consumption data...")
consumption = device.get_power_consumptionX()
# Returns the list of WIFI Network available for the plug
# (Note. this takes some time to complete)
print("\nScanning Wifi...")
wifi_list = device.get_wifi_list()
# Info about the device
print("\nGetting device trace...")
trace = device.get_trace()
print("\nGetting device debug...")
debug = device.get_debug()
# Returns the capabilities of this device
print("\nRetrieving device abilities...")
abilities = device.get_abilities()
# I still have to figure this out :S
# The following command is not yet implemented on all devices
# and might not work as expected.
# report = device.get_report()
# Returns the current power consumption and voltage from the plug
# (Note: this is not really realtime, but close enough)
print("\nReading electricity...")
electricity = device.get_electricity()
```
## Currently supported devices
Even though this library was firstly meant to drive only the Meross MSS310,
other nice developers contributed to its realization. The following is the
currently supported list of devices:
- MSS310 both hw v1 and v2 (Thanks to [DanoneKiD](https://github.com/DanoneKiD))
- MSS210 (Thanks to [ictes](https://github.com/ictes))
- MSS110 (Thanks to [soberstadt](https://github.com/soberstadt))
- MSS425E (Thanks to [ping-localhost](https://github.com/ping-localhost))
## Protocol details
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
## Donate!
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy. However, if you liked or appreciated by work, why don't you buy me a beer? It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
[![Buy me a beer](http://4.bp.blogspot.com/-1Md6-deTZ84/VA_lzcxMx1I/AAAAAAAACl8/wP_4rXBXwyI/s1600/PayPal-Donation-Button.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6HPAB89UYSZF2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" version="9.4.6" editor="www.draw.io" type="github"><diagram id="838e86de-a6c6-3703-056b-65dabb823eb9" name="Page-1">7Vjfj5s4EP5rIt09BBEMIXlMsptrpVbKaVe63tPKCw74ahjOOJukf/2Nwfxme9s23UirJg+xPw9mZr75hpgJ2SSnPyTN4o8QMjFx7PA0ITcTx5m5zhx/NHIukYXtlkAkeWiMGuCOf2EGtA164CHLO4YKQCiedcEA0pQFqoNRKeHYNduD6N41oxEbAHcBFUP0Lx6q2ETh+A3+jvEoru48my/LlYRWxiaSPKYhHFsQuZ2QjQRQ5Sg5bZjQyavyUl63fWa1dkyyVL3oAsPEExUHE9zHP+/vEVlL+MzkxJkL3Gj9qEeRHv3GQVkJk5DnVgDJhKwc2579PjQ0AapzlTU4KMFTtqlJsdFqD6nagABZ2BD8brWn60jSkLNmLYUUt1nnSrs1dsGeC9HC98UH8ZDmMQvN3Z6YVBx5/EAfmdhBzhWHFNceQSkdS22wEjzSCwoyRKmZBegQ5oSsY5UInM9MAKZCZ041N0HrW9I8K6Pd85P2Y42UZ3oxOUVaHRY95q4lWQ4HGbD3gfZnjdNy1LVK/lXqIZOgIABRp1j7zE7PFsCsLivUI4OEKXlGE3OB45tKNFJ0HK+cH5vCnhuTuFXTFUaNlKJ656bacGAK7pniWw6K7939/S5HaLV7P6ifWit2n9YWHd0CMVUjenTXPF6qHvqcJnlAmRUIOIQWzfhDQlPsCUnBSb9Sbdtf3swvw6Y777NpD9j0R9j0L8AmmQ0YYyG2TTMFqWKIIKXitkGxyRzScIxHduLqk4Ytz8z+Nkb/MKXORnH0oAChZu8PoAkqdii90S58PZnocSE9k7HqeUJlxCqz+XjSJRNU8afu/j+SwmqPdje+uyPVY6+rhaLgeFI8qtqZGy/Z/630QiNrGnyOCk7GWmlxs1XVz0abm/HnJlZKP4tXOnRnG4QpsTg2tD1HtmXx3HC2IVUUfzSOkt8esy8AyTRkT0xAxuRUL0xzJMHZejamYEvs6cxZWFkaXaLx2V5HKu5sKBVvRCreJaTivAmpONeUivNLKq8mFf+KUnHfhFTINaVCfknltaRC3CtKxf85UvH7WsFcyPOnyk5PihVrXk13THJ0X/81/iZx9c9wi4AFwdip73HhuZ793XIk3ogcl68kR7L4OTQ539vSrpr1H22CxaUrKem5ZZABT1Xe2nmngdYxyfc7KiWk95akZ09s+2v2OCg9aPivQ3lZSXjDDl28YSlOwrsXvFt5g507AogEmyYU+winAlt4jgFMq2V3Udg+5AmVKovxoP/gLrLTBXu56y2sbjcnyxppH6jJsJ9X2Dc0Cpw27/3KImrenpLb/wA=</diagram></mxfile>

View File

@ -0,0 +1 @@
name = "meross_iot"

View File

@ -0,0 +1,137 @@
import base64
import hashlib
import json
import random
import string
import time
import requests
from meross_iot.device_factory import build_wrapper
# Appears to be used as a part of the signature algorithm as constant "salt" (kinda useless)
_SECRET = "23x17ahWarFH6w29"
_MEROSS_URL = "https://iot.meross.com"
_LOGIN_URL = "%s%s" % (_MEROSS_URL, "/v1/Auth/Login")
_LOG_URL = "%s%s" % (_MEROSS_URL, "/v1/log/user")
_DEV_LIST = "%s%s" % (_MEROSS_URL, "/v1/Device/devList")
class MerossHttpClient:
_token = None
_key = None
_userid = None
_userEmail = None
_email = None
_password = None
_authenticated = False
def __init__(self, email, password):
self._email = email
self._password = password
def _authenticated_post(self,
url, # type: str
params_data # type: dict
):
nonce = self._generate_nonce(16)
timestamp_millis = int(round(time.time() * 1000))
login_params = self._encode_params(params_data)
# Generate the md5-hash (called signature)
m = hashlib.md5()
datatosign = '%s%s%s%s' % (_SECRET, timestamp_millis, nonce, login_params)
m.update(datatosign.encode("utf8"))
md5hash = m.hexdigest()
headers = {
"Authorization": "Basic" if self._token is None else "Basic %s" % self._token,
"vender": "Meross",
"AppVersion": "1.3.0",
"AppLanguage": "EN",
"User-Agent": "okhttp/3.6.0"
}
payload = {
'params': login_params,
'sign': md5hash,
'timestamp': timestamp_millis,
'nonce': nonce
}
# Perform the request.
r = requests.post(url, data=payload, headers=headers)
# Check if that is ok.
if r.status_code != 200:
raise AuthenticatedPostException()
# Save returned value
jsondata = r.json()
# print(jsondata)
if jsondata["info"].lower() != "success":
raise AuthenticatedPostException()
return jsondata["data"]
def _encode_params(self,
parameters # type: dict
):
jsonstring = json.dumps(parameters)
return str(base64.b64encode(jsonstring.encode("utf8")), "utf8")
def _generate_nonce(self, length):
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
def _login(self):
try:
data = {"email": self._email, "password": self._password}
response_data = self._authenticated_post(_LOGIN_URL, params_data=data)
self._token = response_data["token"]
self._key = response_data["key"]
self._userid = response_data["userid"]
self._userEmail = response_data["email"]
self._authenticated = True
except:
return False
try:
# The protocol does not really need the following call. However we want to be nice do it anyways
self._log()
except:
pass
return True
def _log(self, ):
data = {'extra': {}, 'model': 'Android,Android SDK built for x86_64', 'system': 'Android',
'uuid': '493dd9174941ed58waitForOpenWifi', 'vendor': 'Meross', 'version': '6.0'}
response_data = self._authenticated_post(_LOG_URL, params_data=data)
def list_devices(self):
if not self._authenticated and not self._login():
raise UnauthorizedException()
return self._authenticated_post(_DEV_LIST, {})
def list_supported_devices(self):
supported_devices = []
for dev in self.list_devices():
deviceType = dev['deviceType']
device = build_wrapper(self._token, self._key, self._userid, deviceType, dev)
if device is not None:
supported_devices.append(device)
# else log...
return supported_devices
class AuthenticatedPostException(Exception):
pass
class UnauthorizedException(Exception):
pass

View File

@ -0,0 +1,20 @@
from meross_iot.supported_devices.power_plugs import Mss310, Mss210, Mss110, Mss425e
def build_wrapper(
token,
key,
user_id,
device_type, # type: str
device_specs # type: dict
):
if device_type.lower() == "mss310":
return Mss310(token, key, user_id, **device_specs)
elif device_type.lower() == "mss210":
return Mss210(token, key, user_id, **device_specs)
elif device_type.lower() == "mss110":
return Mss110(token, key, user_id, **device_specs)
elif device_type.lower() == "mss425e":
return Mss425e(token, key, user_id, **device_specs)
else:
return None

View File

@ -0,0 +1,412 @@
debug = False
import json
import logging
import random
import ssl
import string
import sys
import time
from enum import Enum
from hashlib import md5
from logging import StreamHandler
from threading import RLock, Condition
import paho.mqtt.client as mqtt
from paho.mqtt import MQTTException
from meross_iot.utilities.synchronization import AtomicCounter
l = logging.getLogger("meross_powerplug")
l.addHandler(StreamHandler(stream=sys.stdout))
l.setLevel(logging.DEBUG)
class ClientStatus(Enum):
INITIALIZED = 1
CONNECTING = 2
CONNECTED = 3
SUBSCRIBED = 4
CONNECTION_DROPPED = 5
class Device:
_status_lock = None
_client_status = None
_token = None
_key = None
_user_id = None
_domain = None
_port = 2001
_channels = []
_uuid = None
_client_id = None
_app_id = None
# Device informations
_name = None
_type = None
_hwversion = None
_fwversion = None
# Topic name where the client should publish to its commands. Every client should have a dedicated one.
_client_request_topic = None
# Topic name in which the client retrieves its own responses from server.
_client_response_topic = None
# Topic where important notifications are pushed (needed if any other client is dealing with the same device)
_user_topic = None
# Paho mqtt client object
_mqtt_client = None
# Waiting condition used to wait for command ACKs
_waiting_message_ack_queue = None
_waiting_subscribers_queue = None
_waiting_message_id = None
_ack_response = None
# Block for at most 10 seconds.
_command_timeout = 10
_error = None
_status = None
def __init__(self,
token,
key,
user_id,
**kwords):
self._status_lock = RLock()
self._waiting_message_ack_queue = Condition()
self._waiting_subscribers_queue = Condition()
self._subscription_count = AtomicCounter(0)
self._set_status(ClientStatus.INITIALIZED)
self._token = token,
self._key = key
self._user_id = user_id
self._uuid = kwords['uuid']
if "domain" in kwords:
self._domain = kwords['domain']
else:
self._domain = "eu-iot.meross.com"
if "channels" in kwords:
self._channels = kwords['channels']
# Informations about device
if "devName" in kwords:
self._name = kwords['devName']
if "deviceType" in kwords:
self._type = kwords['deviceType']
if "fmwareVersion" in kwords:
self._fwversion = kwords['fmwareVersion']
if "hdwareVersion" in kwords:
self._hwversion = kwords['hdwareVersion']
self._generate_client_and_app_id()
# Password is calculated as the MD5 of USERID concatenated with KEY
md5_hash = md5()
clearpwd = "%s%s" % (self._user_id, self._key)
md5_hash.update(clearpwd.encode("utf8"))
hashed_password = md5_hash.hexdigest()
# Start the mqtt client
self._mqtt_client = mqtt.Client(client_id=self._client_id,
protocol=mqtt.MQTTv311) # ex. app-id -> app:08d4c9f99da40203ebc798a76512ec14
self._mqtt_client.on_connect = self._on_connect
self._mqtt_client.on_message = self._on_message
self._mqtt_client.on_disconnect = self._on_disconnect
self._mqtt_client.on_subscribe = self._on_subscribe
self._mqtt_client.on_log = self._on_log
self._mqtt_client.username_pw_set(username=self._user_id, password=hashed_password)
self._mqtt_client.tls_set(ca_certs=None, certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None)
self._mqtt_client.connect(self._domain, self._port, keepalive=30)
self._set_status(ClientStatus.CONNECTING)
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
self._mqtt_client.loop_start()
with self._waiting_subscribers_queue:
self._waiting_subscribers_queue.wait()
if self._client_status != ClientStatus.SUBSCRIBED:
# An error has occurred
raise Exception(self._error)
def _on_disconnect(self, client, userdata, rc):
l.info("Disconnection detected. Reason: %s" % str(rc))
# We should clean all the data structures.
with self._status_lock:
self._subscription_count = AtomicCounter(0)
self._error = "Connection dropped by the server"
self._set_status(ClientStatus.CONNECTION_DROPPED)
with self._waiting_subscribers_queue:
self._waiting_subscribers_queue.notify_all()
with self._waiting_message_ack_queue:
self._waiting_message_ack_queue.notify_all()
if rc == mqtt.MQTT_ERR_SUCCESS:
pass
else:
# TODO: Should we reconnect by calling again the client.loop_start() ?
client.loop_stop()
def _on_unsubscribe(self):
l.info("Unsubscribed from topic")
self._subscription_count.dec()
def _on_subscribe(self, client, userdata, mid, granted_qos):
l.info("Succesfully subscribed!")
if self._subscription_count.inc() == 2:
with self._waiting_subscribers_queue:
self._set_status(ClientStatus.SUBSCRIBED)
self._waiting_subscribers_queue.notify_all()
def _on_connect(self, client, userdata, rc, other):
l.info("Connected with result code %s" % str(rc))
self._set_status(ClientStatus.SUBSCRIBED)
self._set_status(ClientStatus.CONNECTED)
self._client_request_topic = "/appliance/%s/subscribe" % self._uuid
self._client_response_topic = "/app/%s-%s/subscribe" % (self._user_id, self._app_id)
self._user_topic = "/app/%s/subscribe" % self._user_id
# Subscribe to the relevant topics
l.info("Subscribing to topics...")
client.subscribe(self._user_topic)
client.subscribe(self._client_response_topic)
# The callback for when a PUBLISH message is received from the server.
def _on_message(self, client, userdata, msg):
l.debug(msg.topic + " --> " + str(msg.payload))
try:
message = json.loads(str(msg.payload, "utf8"))
header = message['header']
message_hash = md5()
strtohash = "%s%s%s" % (header['messageId'], self._key, header['timestamp'])
message_hash.update(strtohash.encode("utf8"))
expected_signature = message_hash.hexdigest().lower()
if (header['sign'] != expected_signature):
raise MQTTException('The signature did not match!')
# If the message is the RESP for some previous action, process return the control to the "stopped" method.
if header['messageId'] == self._waiting_message_id:
with self._waiting_message_ack_queue:
self._ack_response = message
self._waiting_message_ack_queue.notify()
# Otherwise process it accordingly
elif self._message_from_self(message):
if header['method'] == "PUSH" and 'payload' in message and 'toggle' in message['payload']:
self._handle_toggle(message)
else:
l.debug("UNKNOWN msg received by %s" % self._uuid)
# if header['method'] == "PUSH":
# TODO
else:
# do nothing because the message was from a different device
pass
except Exception as e:
l.debug("%s failed to process message because: %s" % (self._uuid, e))
def _on_log(self, client, userdata, level, buf):
# print("Data: %s - Buff: %s" % (userdata, buf))
pass
def _generate_client_and_app_id(self):
md5_hash = md5()
md5_hash.update(("%s%s" % ("API", self._uuid)).encode("utf8"))
self._app_id = md5_hash.hexdigest()
self._client_id = 'app:%s' % md5_hash.hexdigest()
def _mqtt_message(self, method, namespace, payload):
# Generate a random 16 byte string
randomstring = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(16))
# Hash it as md5
md5_hash = md5()
md5_hash.update(randomstring.encode('utf8'))
messageId = md5_hash.hexdigest().lower()
timestamp = int(round(time.time()))
# Hash the messageId, the key and the timestamp
md5_hash = md5()
strtohash = "%s%s%s" % (messageId, self._key, timestamp)
md5_hash.update(strtohash.encode("utf8"))
signature = md5_hash.hexdigest().lower()
data = {
"header":
{
"from": self._client_response_topic,
"messageId": messageId, # Example: "122e3e47835fefcd8aaf22d13ce21859"
"method": method, # Example: "GET",
"namespace": namespace, # Example: "Appliance.System.All",
"payloadVersion": 1,
"sign": signature, # Example: "b4236ac6fb399e70c3d61e98fcb68b74",
"timestamp": timestamp
},
"payload": payload
}
strdata = json.dumps(data)
l.debug("--> %s" % strdata)
self._mqtt_client.publish(topic=self._client_request_topic, payload=strdata.encode("utf-8"))
return messageId
def _wait_for_status(self, status):
ok = False
while not ok:
if not self._status_lock.acquire(True, self._command_timeout):
raise TimeoutError()
ok = status == self._client_status
self._status_lock.release()
def _set_status(self, status):
with self._status_lock:
self._client_status = status
def _execute_cmd(self, method, namespace, payload):
with self._waiting_subscribers_queue:
while self._client_status != ClientStatus.SUBSCRIBED:
self._waiting_subscribers_queue.wait()
# Execute the command and retrieve the message-id
self._waiting_message_id = self._mqtt_message(method, namespace, payload)
# Wait synchronously until we get the ACK.
with self._waiting_message_ack_queue:
self._waiting_message_ack_queue.wait()
return self._ack_response['payload']
def _message_from_self(self, message):
try:
return 'from' in message['header'] and message['header']['from'].split('/')[2] == self._uuid
except:
return False
def _handle_toggle(self, message):
if 'onoff' in message['payload']['toggle']:
self._status = (message['payload']['toggle']['onoff'] == 1)
def get_sys_data(self):
return self._execute_cmd("GET", "Appliance.System.All", {})
def get_wifi_list(self):
return self._execute_cmd("GET", "Appliance.Config.WifiList", {})
def get_trace(self):
return self._execute_cmd("GET", "Appliance.Config.Trace", {})
def get_debug(self):
return self._execute_cmd("GET", "Appliance.System.Debug", {})
def get_abilities(self):
return self._execute_cmd("GET", "Appliance.System.Ability", {})
def get_report(self):
return self._execute_cmd("GET", "Appliance.System.Report", {})
def get_status(self):
if self._status is None:
self._status = self.get_sys_data()['all']['control']['toggle']['onoff'] == 1
return self._status
def device_id(self):
return self._uuid
def get_channels(self):
return self._channels
def turn_on(self):
self._status = True
payload = {"channel": 0, "toggle": {"onoff": 1}}
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
def turn_off(self):
self._status = False
payload = {"channel": 0, "toggle": {"onoff": 0}}
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
class Mss310(Device):
def get_power_consumptionX(self):
return self._execute_cmd("GET", "Appliance.Control.ConsumptionX", {})
def get_electricity(self):
return self._execute_cmd("GET", "Appliance.Control.Electricity", {})
def turn_on(self):
if self._hwversion.split(".")[0] == "2":
payload = {'togglex': {"onoff": 1}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
else:
payload = {"channel": 0, "toggle": {"onoff": 1}}
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
def turn_off(self):
if self._hwversion.split(".")[0] == "2":
payload = {'togglex': {"onoff": 0}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
else:
payload = {"channel": 0, "toggle": {"onoff": 0}}
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
class Mss425e(Device):
# TODO Implement for all channels
def _handle_toggle(self, message):
return None
# TODO Implement for all channels
def get_status(self):
return None
def turn_on(self):
payload = {'togglex': {"onoff": 1}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
def turn_off(self):
payload = {'togglex': {"onoff": 0}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
def turn_on_channel(self, channel):
payload = {'togglex': {'channel': channel, 'onoff': 1}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
def turn_off_channel(self, channel):
payload = {'togglex': {'channel': channel, 'onoff': 0}}
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
def enable_usb(self):
return self.turn_on_channel(4)
def disable_usb(self):
return self.turn_off_channel(4)
class Mss210(Device):
pass
class Mss110(Device):
pass

View File

@ -0,0 +1,19 @@
from threading import RLock
class AtomicCounter(object):
_lock = None
def __init__(self, initialValue):
self._lock = RLock()
self._val = initialValue
def dec(self):
with self._lock:
self._val -= 1
return self._val
def inc(self):
with self._lock:
self._val += 1
return self._val

View File

@ -0,0 +1,2 @@
paho-mqtt>=1.3.1
requests>=2.19.1

View File

@ -0,0 +1,40 @@
from os import path
from setuptools import setup, find_packages
here = path.abspath(path.dirname(__file__))
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
setup(
name='meross_iot',
version='0.1.4.2',
packages=find_packages(exclude=('tests',)),
url='https://github.com/albertogeniola/MerossIot',
license='MIT',
author='Alberto Geniola',
author_email='albertogeniola@gmail.com',
classifiers=[
'Intended Audience :: Developers',
'Programming Language :: Python :: 3',
'Operating System :: OS Independent'
],
description='A simple library to deal with Meross devices. At the moment MSS110, MSS210, MSS310 smart plugs and '
'the MSS425E power strip',
long_description=long_description,
long_description_content_type='text/markdown',
keywords='meross smartplug iot mqtt domotic switch mss310 mss210 mss110 mss425e',
project_urls={
'Documentation': 'https://github.com/albertogeniola/MerossIot',
'Funding': 'https://donate.pypi.org',
'Source': 'https://github.com/albertogeniola/MerossIot',
'Tracker': 'https://github.com/albertogeniola/MerossIot/issues',
},
install_requires=[
'paho-mqtt>=1.3.1',
'requests>=2.19.1'
],
python_requires='>=3',
test_suite='tests'
)

View File

@ -0,0 +1,53 @@
import time
import sys
from meross_iot.api import MerossHttpClient
if __name__=='__main__':
httpHandler = MerossHttpClient(email="thomas.fransolet@hotmail.be", password="Awesome09")
# Retrieves the list of supported devices
print("Listing Devices...")
devices = httpHandler.list_supported_devices()
for counter, device in enumerate(devices):
print("Playing with device: %d" % counter)
# Returns most of the info about the power plug
print("\nGetting system data...")
data = device.get_sys_data()
# Turns the power-plug on
print("\nTurning the device on...")
device.turn_off()
# Turns the power-plug off
print("\nTurning the device off...")
device.turn_on()
# Reads the historical device consumption
print("\nReading consumption data...")
consumption = device.get_power_consumptionX()
# Returns the list of WIFI Network available for the plug
# (Note. this takes some time to complete)
print("\nScanning Wifi...")
wifi_list = device.get_wifi_list()
# Info about the device
print("\nGetting device trace...")
trace = device.get_trace()
print("\nGetting device debug...")
debug = device.get_debug()
# Returns the capabilities of this device
print("\nRetrieving device abilities...")
abilities = device.get_abilities()
# I still have to figure this out :S
# The following command is not yet implemented on all devices
# and might not work as expected.
# report = device.get_report()
# Returns the current power consumption and voltage from the plug
# (Note: this is not really realtime, but close enough)
print("\nReading electricity...")
electricity = device.get_electricity()

View File

@ -0,0 +1,69 @@
import os
import time
import unittest
from meross_iot.api import MerossHttpClient
from meross_iot.supported_devices.power_plugs import Mss310
EMAIL = os.environ.get('thomas.fransolet@hotmail.be')
PASSWORD = os.environ.get('Awesome09')
class TestHttpMethods(unittest.TestCase):
def setUp(self):
self.client = MerossHttpClient(email=EMAIL, password=PASSWORD)
def test_device_listing(self):
devices = self.client.list_devices()
assert devices is not None
assert len(devices) > 0
def test_supported_device_listing(self):
devices = self.client.list_supported_devices()
assert devices is not None
assert len(devices) > 0
class TestMSS310Test(unittest.TestCase):
def setUp(self):
httpHandler = MerossHttpClient(email=EMAIL, password=PASSWORD)
# Retrieves the list of supported devices
devices = httpHandler.list_supported_devices()
for counter, device in enumerate(devices):
if isinstance(device, Mss310):
self.device = device
break
def test_power_cycle(self):
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
def test_get_info(self):
consumption = self.device.get_power_consumptionX()
assert consumption is not None
wifi_list = self.device.get_wifi_list()
assert wifi_list is not None
trace = self.device.get_trace()
assert trace is not None
debug = self.device.get_debug()
assert debug is not None
abilities = self.device.get_abilities()
assert abilities is not None
electricity = self.device.get_electricity()
assert electricity is not None

@ -1 +0,0 @@
Subproject commit 0e49e69854f060518309892ebb8684e9dea457bd

Binary file not shown.

115
RPI Code/Meross_2/MerossIot_/.gitignore vendored Normal file
View File

@ -0,0 +1,115 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Local test
local_test/*
utilities/InfoGather.py
# Dev utils
ext-res/tests/node_modules
ext-res/tests/package-lock.json
# IntelliJ
.idea

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Alberto Geniola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,269 @@
![Azure DevOps builds (branch)](https://img.shields.io/azure-devops/build/albertogeniola/c4128d1b-c23c-418d-95c5-2de061954ee5/1/0.3.X.X.svg)
![Deployment](https://albertogeniola.vsrm.visualstudio.com/_apis/public/Release/badge/c4128d1b-c23c-418d-95c5-2de061954ee5/1/1)
![Test status](https://img.shields.io/azure-devops/tests/albertogeniola/meross/1/master.svg)
[![PyPI version](https://badge.fury.io/py/meross-iot.svg)](https://badge.fury.io/py/meross-iot)
[![Downloads](https://pepy.tech/badge/meross-iot)](https://pepy.tech/project/meross-iot)
![PyPI - Downloads](https://img.shields.io/pypi/dm/meross-iot.svg?label=Pypi%20Downloads)
[![Beerpay](https://beerpay.io/albertogeniola/MerossIot/badge.svg?style=flat)](https://beerpay.io/albertogeniola/MerossIot)
# Meross IoT library
A pure-python based library providing API for controlling Meross IoT devices over the internet.
To see what devices are currently supported, checkout the *Currently supported devices* section.
However, some devices _might work as expected even if they are not listed_ among the supported devices.
In such cases, you're invited to open an issue and report tbe working/non-working status of your device.
This will help us to keep track of new devices and current support status of the library.
This library is still work in progress, therefore use it with caution.
## Installation
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
So, the installation is as simple as typing the following command:
```
pip install meross_iot==0.3.1.3 --upgrade
```
## Usage
The following script demonstrates how to use this library.
```python
from meross_iot.manager import MerossManager
from meross_iot.meross_event import MerossEventType
from meross_iot.cloud.devices.light_bulbs import GenericBulb
from meross_iot.cloud.devices.power_plugs import GenericPlug
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
import time
import os
EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
def event_handler(eventobj):
if eventobj.event_type == MerossEventType.DEVICE_ONLINE_STATUS:
print("Device online status changed: %s went %s" % (eventobj.device.name, eventobj.status))
pass
elif eventobj.event_type == MerossEventType.DEVICE_SWITCH_STATUS:
print("Switch state changed: Device %s (channel %d) went %s" % (eventobj.device.name, eventobj.channel_id,
eventobj.switch_state))
else:
print("Unknown event!")
if __name__=='__main__':
# Initiates the Meross Cloud Manager. This is in charge of handling the communication with the remote endpoint
manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
# Register event handlers for the manager...
manager.register_event_handler(event_handler)
# Starts the manager
manager.start()
# You can retrieve the device you are looking for in various ways:
# By kind
bulbs = manager.get_devices_by_kind(GenericBulb)
plugs = manager.get_devices_by_kind(GenericPlug)
door_openers = manager.get_devices_by_kind(GenericGarageDoorOpener)
all_devices = manager.get_supported_devices()
# Print some basic specs about the discovered devices
print("All the bulbs I found:")
for b in bulbs:
print(b)
print("All the plugs I found:")
for p in plugs:
print(p)
print("All the garage openers I found:")
for g in door_openers:
print(g)
print("All the supported devices I found:")
for d in all_devices:
print(d)
# You can also retrieve devices by the UUID/name
# a_device = manager.get_device_by_name("My Plug")
# a_device = manager.get_device_by_uuid("My Plug")
# Or you can retrieve all the device by the HW type
# all_mss310 = manager.get_devices_by_type("mss310")
# ------------------------------
# Let's play the garage openers.
# ------------------------------
for g in door_openers:
if not g.online:
print("The garage controller %s seems to be offline. Cannot play with that..." % g.name)
continue
print("Opening door %s..." % g.name)
g.open_door()
print("Closing door %s..." % g.name)
g.close_door()
# ---------------------
# Let's play with bulbs
# ---------------------
for b in bulbs: # type: GenericBulb
if not b.online:
print("The bulb %s seems to be offline. Cannot play with that..." % b.name)
continue
print("Let's play with bulb %s" % b.name)
if not b.supports_light_control():
print("Too bad bulb %s does not support light control %s" % b.name)
else:
# Let's make it red!
b.set_light_color(rgb=(255, 0, 0))
b.turn_on()
time.sleep(1)
b.turn_off()
# ---------------------------
# Let's play with smart plugs
# ---------------------------
for p in plugs: # type: GenericPlug
if not p.online:
print("The plug %s seems to be offline. Cannot play with that..." % p.name)
continue
print("Let's play with smart plug %s" % p.name)
channels = len(p.get_channels())
print("The plug %s supports %d channels." % (p.name, channels))
for i in range(0, channels):
print("Turning on channel %d of %s" % (i, p.name))
p.turn_on_channel(i)
time.sleep(1)
print("Turning off channel %d of %s" % (i, p.name))
p.turn_off_channel(i)
usb_channel = p.get_usb_channel_index()
if usb_channel is not None:
print("Awesome! This device also supports USB power.")
p.enable_usb()
time.sleep(1)
p.disable_usb()
if p.supports_electricity_reading():
print("Awesome! This device also supports power consumption reading.")
print("Current consumption is: %s" % str(p.get_electricity()))
# At this point, we are all done playing with the library, so we gracefully disconnect and clean resources.
print("We are done playing. Cleaning resources...")
manager.stop()
print("Bye bye!")
```
## Currently supported devices
Starting from v0.2.0.0, this library should support the majority of Meross devices on the market.
The list of tested devices is the following:
- MSL120
- MSS110
- MSS210
- MSS310
- MSS310h
- MSS425e
- MSS530H
- MSG100
I'd like to thank all the people who contributed to the early stage of library development,
who stimulated me to continue the development and making this library support more devices:
Thanks to [DanoneKiD](https://github.com/DanoneKiD), [virtualdj](https://github.com/virtualdj), [ictes](https://github.com/ictes), [soberstadt](https://github.com/soberstadt), [ping-localhost](https://github.com/ping-localhost).
## Protocol details
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
## Homeassistant integration
Yeah, it happened. As soon as I started developing this library, I've discovered the HomeAssistant world.
Thus, I've decided to spend some time to develop a full featured Homeassistant custom component, that you find [here](https://github.com/albertogeniola/meross-homeassistant).
Thanks to @troykelly who made a wish and supported my efforts in developing such component!
## Donate!
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy.
However, if you liked or appreciated by work, why don't you buy me a beer?
It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
Moreover, donations will make me raise money to spend on other Meross devices.
So far, I've bought the following devices:
- MSL120
- MSS210
- MSS310
- MSS425E
- MSS530H
- MSG100
By issuing a donation, you will:
1. Give me the opportunity to buy new devices and support them in this library
1. Pay part of electricity bill used to keep running the plugs 24/7
(Note that they are used for Unit-Testing on the continuous integration engine when someone pushes a PR... I love DEVOPing!)
1. You'll increase the quality of my coding sessions with free-beer!
[![Buy me a beer](http://4.bp.blogspot.com/-1Md6-deTZ84/VA_lzcxMx1I/AAAAAAAACl8/wP_4rXBXwyI/s1600/PayPal-Donation-Button.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CQQCK3RN32BHL&source=url)
[![Beerpay](https://beerpay.io/albertogeniola/MerossIot/badge.svg?style=beer-square)](https://beerpay.io/albertogeniola/MerossIot) [![Beerpay](https://beerpay.io/albertogeniola/MerossIot/make-wish.svg?style=flat-square)](https://beerpay.io/albertogeniola/MerossIot?focus=wish)
### Look at these babies!
<p>
Look at the test environment that ensures high quality code of the library!
</p>
<img src="ext-res/plugs/test-env.jpg" alt="Current test environemnt" width="400" />
<p>When a pull-request is performed against this repository, a CI pipeline takes care of building the code,
testing it on Python 3.5/3.6/3.7, relying on some junit tests and, if all the tests pass as expected, the library
is released on Pypi. However, to ensure that the code <i>really works</i>,
the pipeline will issue on/off commands against real devices, that are dedicated 24/7 to the tests.
Such devices have been bought by myself (with contributions received by donators).
However, keeping such devices connected 24/7 has a cost, which I sustain happily due to the success of the library.
Anyways, feel free to contribute via donations!
</p>
## Changelog
### 0.3.1.3
- Added event fire capability to GenericBulb class.
- Fixed bulb state kwargs bug
- Improved set_light_color function for bulbs
### 0.3.0.2
- Fixed door closing checks when using the async + callback close() and open() methods.
### 0.3.0.1
- Added get_power_consumption() and get_electricity() methods as abstract methods of AbstractMerossDevice
- Fixed regression passing manager parameter when firing Meross events.
### 0.3.0.0rc4
- Added added switch_state to the generated event
### 0.3.0.0rc3
- Added quick fix for MSS560 color control
### 0.3.0.0rc2
- Fixed Major bugs with MSG100
- Updated README examples
### 0.3.0.0rc1
- Added MSG100 support
- Fixed errors being logged when power consumptionX command was issued on powerplugs
### 0.3.0.0b1
- General refactor of the library
- Added event-based support
- Fixed default mqtt broker address for non-european devices
### 0.2.2.1
- Added basic bulb support: turning on/off and light control
- Implemented MSL120 support
- Implemented MSL120 automatic test
- Extended example script usage to show how to control the light bulbs
- Added maximum retry limit for execute_command and connect()
### 0.2.1.1
- Code refactoring to support heterogeneous devices (bulbs, plugs, garage openers)
### 0.2.1.0
- Implemented auto-reconnect on lost connection
- Improving locking system in order to prevent library hangs when no ack is received

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" version="9.4.6" editor="www.draw.io" type="github"><diagram id="838e86de-a6c6-3703-056b-65dabb823eb9" name="Page-1">7Vjfj5s4EP5rIt09BBEMIXlMsptrpVbKaVe63tPKCw74ahjOOJukf/2Nwfxme9s23UirJg+xPw9mZr75hpgJ2SSnPyTN4o8QMjFx7PA0ITcTx5m5zhx/NHIukYXtlkAkeWiMGuCOf2EGtA164CHLO4YKQCiedcEA0pQFqoNRKeHYNduD6N41oxEbAHcBFUP0Lx6q2ETh+A3+jvEoru48my/LlYRWxiaSPKYhHFsQuZ2QjQRQ5Sg5bZjQyavyUl63fWa1dkyyVL3oAsPEExUHE9zHP+/vEVlL+MzkxJkL3Gj9qEeRHv3GQVkJk5DnVgDJhKwc2579PjQ0AapzlTU4KMFTtqlJsdFqD6nagABZ2BD8brWn60jSkLNmLYUUt1nnSrs1dsGeC9HC98UH8ZDmMQvN3Z6YVBx5/EAfmdhBzhWHFNceQSkdS22wEjzSCwoyRKmZBegQ5oSsY5UInM9MAKZCZ041N0HrW9I8K6Pd85P2Y42UZ3oxOUVaHRY95q4lWQ4HGbD3gfZnjdNy1LVK/lXqIZOgIABRp1j7zE7PFsCsLivUI4OEKXlGE3OB45tKNFJ0HK+cH5vCnhuTuFXTFUaNlKJ656bacGAK7pniWw6K7939/S5HaLV7P6ifWit2n9YWHd0CMVUjenTXPF6qHvqcJnlAmRUIOIQWzfhDQlPsCUnBSb9Sbdtf3swvw6Y777NpD9j0R9j0L8AmmQ0YYyG2TTMFqWKIIKXitkGxyRzScIxHduLqk4Ytz8z+Nkb/MKXORnH0oAChZu8PoAkqdii90S58PZnocSE9k7HqeUJlxCqz+XjSJRNU8afu/j+SwmqPdje+uyPVY6+rhaLgeFI8qtqZGy/Z/630QiNrGnyOCk7GWmlxs1XVz0abm/HnJlZKP4tXOnRnG4QpsTg2tD1HtmXx3HC2IVUUfzSOkt8esy8AyTRkT0xAxuRUL0xzJMHZejamYEvs6cxZWFkaXaLx2V5HKu5sKBVvRCreJaTivAmpONeUivNLKq8mFf+KUnHfhFTINaVCfknltaRC3CtKxf85UvH7WsFcyPOnyk5PihVrXk13THJ0X/81/iZx9c9wi4AFwdip73HhuZ793XIk3ogcl68kR7L4OTQ539vSrpr1H22CxaUrKem5ZZABT1Xe2nmngdYxyfc7KiWk95akZ09s+2v2OCg9aPivQ3lZSXjDDl28YSlOwrsXvFt5g507AogEmyYU+winAlt4jgFMq2V3Udg+5AmVKovxoP/gLrLTBXu56y2sbjcnyxppH6jJsJ9X2Dc0Cpw27/3KImrenpLb/wA=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,12 @@
{
"name": "lab-master-switch",
"version": "0.0.1",
"email": "albertogeniola@gmail.com",
"main": "switch.js",
"bin": "./switch.js",
"dependencies": {
"minimist": ">=1.2.0",
"tplink-cloud-api": ">=0.3.8",
"uuid": ">=3.3.2"
}
}

View File

@ -0,0 +1,55 @@
const minimist = require('minimist');
const { login } = require("tplink-cloud-api");
const uuidV4 = require("uuid/v4");
const TPLINK_USER = process.env.TPLINK_USER;
const TPLINK_PASS = process.env.TPLINK_PASS;
const TPLINK_TERM = process.env.TPLINK_TERM || uuidV4();
async function main() {
var args = minimist(process.argv.slice(2));
// Check args
var username;
if ('username' in args) {
username = args.username;
} else if (TPLINK_USER) {
username = TPLINK_USER;
} else {
console.error("Missing username. You can either set the user name to the environment variable TPLINK_USER or pass it via --username parameter");
process.exit(1)
}
var password;
if ('password' in args) {
password = args.password;
} else if (TPLINK_PASS) {
password = TPLINK_PASS;
} else {
console.error("Missing password. You can either set the user name to the environment variable TPLINK_PASS or pass it via --password parameter");
process.exit(1)
}
if (!'toggle' in args) {
console.error("Missing required parameter: --toggle");
process.exit(1)
}
if (!'dev' in args) {
console.error("Missing required parameter: --dev");
process.exit(1)
}
// log in to cloud, return a connected tplink object
const tplink = await login(username, password, TPLINK_TERM);
let deviceList = await tplink.getDeviceList();
console.log("Controlling", args.dev);
if (args.toggle==='on' || args.toggle===1)
await tplink.getHS110(args.dev).powerOn();
else if (args.toggle === 'off' || args.toggle===0)
await tplink.getHS110(args.dev).powerOff();
}
main();

View File

@ -0,0 +1 @@
name = "meross_iot"

View File

@ -0,0 +1,133 @@
import base64
import hashlib
import json
import random
import string
import time
import requests
from meross_iot.credentials import MerossCloudCreds
# Appears to be used as a part of the signature algorithm as constant "salt" (kinda useless)
_SECRET = "23x17ahWarFH6w29"
_MEROSS_URL = "https://iot.meross.com"
_LOGIN_URL = "%s%s" % (_MEROSS_URL, "/v1/Auth/Login")
_LOG_URL = "%s%s" % (_MEROSS_URL, "/v1/log/user")
_DEV_LIST = "%s%s" % (_MEROSS_URL, "/v1/Device/devList")
class MerossHttpClient:
_cloud_creds = None
_email = None
_password = None
_authenticated = False
def __init__(self, email, password):
self._email = email
self._password = password
def _authenticated_post(self,
url, # type: str
params_data # type: dict
):
nonce = self._generate_nonce(16)
timestamp_millis = int(round(time.time() * 1000))
login_params = self._encode_params(params_data)
# Generate the md5-hash (called signature)
m = hashlib.md5()
datatosign = '%s%s%s%s' % (_SECRET, timestamp_millis, nonce, login_params)
m.update(datatosign.encode("utf8"))
md5hash = m.hexdigest()
headers = {
"Authorization": "Basic" if self._cloud_creds is None else "Basic %s" % self._cloud_creds.token,
"vender": "Meross",
"AppVersion": "1.3.0",
"AppLanguage": "EN",
"User-Agent": "okhttp/3.6.0"
}
payload = {
'params': login_params,
'sign': md5hash,
'timestamp': timestamp_millis,
'nonce': nonce
}
# Perform the request.
r = requests.post(url, data=payload, headers=headers)
# Check if that is ok.
if r.status_code != 200:
raise AuthenticatedPostException()
# Save returned value
jsondata = r.json()
# print(jsondata)
if jsondata["info"].lower() != "success":
raise AuthenticatedPostException()
return jsondata["data"]
@staticmethod
def _encode_params(parameters # type: dict
):
jsonstring = json.dumps(parameters)
return str(base64.b64encode(jsonstring.encode("utf8")), "utf8")
@staticmethod
def _generate_nonce(length):
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
def _login(self):
try:
data = {"email": self._email, "password": self._password}
response_data = self._authenticated_post(_LOGIN_URL, params_data=data)
creds = MerossCloudCreds()
creds.token = response_data["token"]
creds.key = response_data["key"]
creds.user_id = response_data["userid"]
creds.user_email = response_data["email"]
self._cloud_creds = creds
self._authenticated = True
except Exception as e:
# TODO: LOG
return False
try:
# The protocol does not really need the following call. However we want to be nice do it anyways
self._log()
except:
# TODO: LOG
pass
return True
def _log(self):
data = {'extra': {}, 'model': 'Android,Android SDK built for x86_64', 'system': 'Android',
'uuid': '493dd9174941ed58waitForOpenWifi', 'vendor': 'Meross', 'version': '6.0'}
self._authenticated_post(_LOG_URL, params_data=data)
def list_devices(self):
if not self._authenticated and not self._login():
raise UnauthorizedException()
return self._authenticated_post(_DEV_LIST, {})
def get_cloud_credentials(self):
if not self._authenticated and not self._login():
raise UnauthorizedException()
return self._cloud_creds
class AuthenticatedPostException(Exception):
pass
class UnauthorizedException(Exception):
pass

View File

@ -0,0 +1,22 @@
# Common abilities
ALL = 'Appliance.System.All'
ABILITY = 'Appliance.System.Ability'
REPORT = 'Appliance.System.Report'
ONLINE = 'Appliance.System.Online'
WIFI_LIST = 'Appliance.Config.WifiList'
DEBUG = 'Appliance.System.Debug'
TRACE = 'Appliance.Config.Trace'
# Power plug/bulbs abilities
TOGGLE = 'Appliance.Control.Toggle'
TOGGLEX = 'Appliance.Control.ToggleX'
TRIGGER = 'Appliance.Control.Trigger'
TRIGGERX = 'Appliance.Control.TriggerX'
ELECTRICITY = 'Appliance.Control.Electricity'
CONSUMPTIONX = 'Appliance.Control.ConsumptionX'
# Garage opener abilities
GARAGE_DOOR_STATE = 'Appliance.GarageDoor.State'
# Bulbs-only abilities
LIGHT = 'Appliance.Control.Light'

View File

@ -0,0 +1,356 @@
from hashlib import md5
import string
from threading import Event, RLock
import json
import uuid as UUID
import ssl
import random
import copy
import time
from meross_iot.cloud.timeouts import SHORT_TIMEOUT
from meross_iot.cloud.exceptions.CommandTimeoutException import CommandTimeoutException
from meross_iot.logger import CONNECTION_MANAGER_LOGGER as l
import paho.mqtt.client as mqtt
from meross_iot.credentials import MerossCloudCreds
from meross_iot.cloud.client_status import ClientStatus
from meross_iot.cloud.connection import ConnectionStatusManager
from meross_iot.utilities.synchronization import AtomicCounter
from meross_iot.logger import NETWORK_DATA as networkl
def build_client_request_topic(client_uuid):
return "/appliance/%s/subscribe" % client_uuid
class PendingMessageResponse(object):
"""
This class is used as an Handle for mqtt messages that expect an ACK back.
When a callback is passed to the constructor, this object is configured as an "async" waiter.
Instead, passing a None callback, makes this object to act as a synchronously waiter.
It is meant to be used internally by the library, in order to handle ACK waiting and callback calling.
Note that this object is not thread safe.
"""
_message_id = None
_callback = None
_event = None
_response = None
_error = None
def __init__(self, message_id, callback=None):
self._message_id = message_id
# Only instantiate an event if no callback has been specified
if callback is None:
self._event = Event()
else:
self._callback = callback
def wait_for_response(self, timeout=SHORT_TIMEOUT):
"""
This method blocks until an ACK/RESPONSE message is received for the corresponding message_id that it refers
to. Note that this method only works when the user is synchronously waiting for the response message.
This method raises an exception if invoked when a callback was specified in the constructor.
:param timeout:
:return:
"""
if self._event is None:
raise Exception("Error: you can invoke this method only if you don't use a callback (i.e. sync invocation)")
# Wait until we receive the message.
# If timeout occurs, return failure and None as received message.
success = self._event.wait(timeout=timeout)
return success, self._response
def notify_message_received(self, error=None, response=None):
self._response = copy.deepcopy(response)
self._error = error
if self._event is not None:
self._event.set()
elif self._callback is not None:
try:
self._callback(self._error, self._response)
except:
l.exception("Unhandled error occurred while executing the callback")
class MerossCloudClient(object):
# Meross Cloud credentials, which are provided by the HTTP Api.
_cloud_creds = None
# Connection info
connection_status = None
_domain = None
_port = 2001
_ca_cert = None
# App and client ID
_app_id = None
_client_id = None
# Paho mqtt client object
_mqtt_client = None
# Callback to be invoked every time a push notification is received from the MQTT broker
_push_message_callback = None
# This dictionary is used to keep track of messages issued to the broker that are waiting for an ACK
# The key is the message_id, the value is the PendingMessageResponse object.
# Access to this resource is protected with exclusive locking
_pending_response_messages = None
_pending_responses_lock = None
def __init__(self,
cloud_credentials, # type: MerossCloudCreds
push_message_callback=None, # type: callable
**kwords):
self.connection_status = ConnectionStatusManager()
self._cloud_creds = cloud_credentials
self._pending_response_messages = dict()
self._pending_responses_lock = RLock()
self._push_message_callback = push_message_callback
self._subscription_count = AtomicCounter(0)
if "domain" in kwords:
self._domain = kwords['domain']
else:
self._domain = "iot.meross.com"
# Lookup port and certificate for MQTT server
self._port = kwords.get('port', MerossCloudClient._port)
self._ca_cert = kwords.get('ca_cert', None)
self._generate_client_and_app_id()
# Password is calculated as the MD5 of USERID concatenated with KEY
md5_hash = md5()
clearpwd = "%s%s" % (self._cloud_creds.user_id, self._cloud_creds.key)
md5_hash.update(clearpwd.encode("utf8"))
hashed_password = md5_hash.hexdigest()
# Start the mqtt client
self._mqtt_client = mqtt.Client(client_id=self._client_id,
protocol=mqtt.MQTTv311) # ex. app-id -> app:08d4c9f99da40203ebc798a76512ec14
self._mqtt_client.on_connect = self._on_connect
self._mqtt_client.on_message = self._on_message
self._mqtt_client.on_disconnect = self._on_disconnect
self._mqtt_client.on_subscribe = self._on_subscribe
# Avoid login if user_id is None
if self._cloud_creds.user_id is not None:
self._mqtt_client.username_pw_set(username=self._cloud_creds.user_id,
password=hashed_password)
self._mqtt_client.tls_set(ca_certs=self._ca_cert, certfile=None,
keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None)
def close(self):
l.info("Closing the MQTT connection...")
self._mqtt_client.disconnect()
l.debug("Waiting for the client to disconnect...")
self.connection_status.wait_for_status(ClientStatus.CONNECTION_DROPPED)
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
l.debug("Stopping the MQTT looper.")
self._mqtt_client.loop_stop(True)
l.info("Client has been fully disconnected.")
def connect(self):
"""
Starts the connection to the MQTT broker
:return:
"""
l.info("Initializing the MQTT connection...")
self._mqtt_client.connect(self._domain, self._port, keepalive=30)
self.connection_status.update_status(ClientStatus.CONNECTING)
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
l.debug("(Re)Starting the MQTT looper.")
self._mqtt_client.loop_stop(True)
self._mqtt_client.loop_start()
l.debug("Waiting for the client to connect...")
self.connection_status.wait_for_status(ClientStatus.SUBSCRIBED)
l.info("Client connected to MQTT broker and subscribed to relevant topics.")
# ------------------------------------------------------------------------------------------------
# MQTT Handlers
# ------------------------------------------------------------------------------------------------
def _on_disconnect(self, client, userdata, rc):
l.info("Disconnection detected. Reason: %s" % str(rc))
# When the mqtt connection is dropped, we need to reset the subscription counter.
self._subscription_count = AtomicCounter(0)
self.connection_status.update_status(ClientStatus.CONNECTION_DROPPED)
# TODO: should we handle disconnection in some way at this level?
if rc == mqtt.MQTT_ERR_SUCCESS:
pass
else:
client.loop_stop(True)
def _on_unsubscribe(self):
l.debug("Unsubscribed from topic")
self._subscription_count.dec()
def _on_subscribe(self, client, userdata, mid, granted_qos):
l.debug("Succesfully subscribed to topic. Subscription count: %d" % self._subscription_count.get())
if self._subscription_count.inc() == 2:
self.connection_status.update_status(ClientStatus.SUBSCRIBED)
def _on_connect(self, client, userdata, rc, other):
l.debug("Connected with result code %s" % str(rc))
self.connection_status.update_status(ClientStatus.CONNECTED)
self._client_response_topic = "/app/%s-%s/subscribe" % (self._cloud_creds.user_id, self._app_id)
self._user_topic = "/app/%s/subscribe" % self._cloud_creds.user_id
# Subscribe to the relevant topics
l.debug("Subscribing to topics...")
client.subscribe(self._user_topic)
client.subscribe(self._client_response_topic)
def _on_message(self, client, userdata, msg):
"""
This handler is called when a message is received from the MQTT broker, on the subscribed topics.
The current implementation checks the validity of the message itself, by verifying its signature.
:param client: is the MQTT client reference, useful to respond back
:param userdata: metadata about the received data
:param msg: message that was received
:return: nothing, it simply handles the message accordingly.
"""
networkl.debug(msg.topic + " --> " + str(msg.payload))
try:
message = json.loads(str(msg.payload, "utf8"))
header = message['header']
message_hash = md5()
strtohash = "%s%s%s" % (header['messageId'], self._cloud_creds.key, header['timestamp'])
message_hash.update(strtohash.encode("utf8"))
expected_signature = message_hash.hexdigest().lower()
if header['sign'] != expected_signature:
# TODO: custom exception for invalid signature
raise Exception('The signature did not match!')
# Check if there is any thread waiting for this message or if there is a callback that we need to invoke.
# If so, do it here.
handle = None
with self._pending_responses_lock:
msg_id = header['messageId']
handle = self._pending_response_messages.get(msg_id)
from_myself = False
if handle is not None:
# There was a handle for this message-id. It means it is a response message to some
# request performed by the library itself.
from_myself = True
try:
l.debug("Calling handle event handler for message %s" % msg_id)
# Call the handler
handle.notify_message_received(error=None, response=message)
l.debug("Done handler for message %s" % msg_id)
# Remove the message from the pending queue
with self._pending_responses_lock:
del self._pending_response_messages[msg_id]
except:
l.exception("Error occurred while invoking message handler")
# Let's also catch all the "PUSH" notifications and dispatch them to the push_notification_callback.
if self._push_message_callback is not None and header['method'] == "PUSH" and 'namespace' in header:
self._push_message_callback(message, from_myself=from_myself)
except Exception:
l.exception("Failed to process message.")
# ------------------------------------------------------------------------------------------------
# Protocol Handlers
# ------------------------------------------------------------------------------------------------
def execute_cmd(self, dst_dev_uuid, method, namespace, payload, callback=None, timeout=SHORT_TIMEOUT):
start = time.time()
# Build the mqtt message we will send to the broker
message, message_id = self._build_mqtt_message(method, namespace, payload)
# Register the waiting handler for that message
handle = PendingMessageResponse(message_id=message_id, callback=callback)
with self._pending_responses_lock:
self._pending_response_messages[message_id] = handle
# Send the message to the broker
l.debug("Executing message-id %s, %s on %s command for device %s" % (message_id, method,
namespace, dst_dev_uuid))
self._mqtt_client.publish(topic=build_client_request_topic(dst_dev_uuid), payload=message)
# If the caller has specified a callback, we don't need to actrively wait for the message ACK. So we can
# immediately return.
if callback is not None:
return None
# Otherwise, we need to wait until the message is received.
l.debug("Waiting for response to message-id %s" % message_id)
success, resp = handle.wait_for_response(timeout=timeout)
if not success:
raise CommandTimeoutException("A timeout occurred while waiting for the ACK: %d" % timeout)
elapsed = time.time() - start
l.debug("Message-id: %s, command %s-%s command for device %s took %s" % (message_id, method,
namespace, dst_dev_uuid, str(elapsed)))
return resp['payload']
# ------------------------------------------------------------------------------------------------
# Protocol utilities
# ------------------------------------------------------------------------------------------------
def _build_mqtt_message(self, method, namespace, payload):
"""
Sends a message to the Meross MQTT broker, respecting the protocol payload.
:param method:
:param namespace:
:param payload:
:return:
"""
# Generate a random 16 byte string
randomstring = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(16))
# Hash it as md5
md5_hash = md5()
md5_hash.update(randomstring.encode('utf8'))
messageId = md5_hash.hexdigest().lower()
timestamp = int(round(time.time()))
# Hash the messageId, the key and the timestamp
md5_hash = md5()
strtohash = "%s%s%s" % (messageId, self._cloud_creds.key, timestamp)
md5_hash.update(strtohash.encode("utf8"))
signature = md5_hash.hexdigest().lower()
data = {
"header":
{
"from": self._client_response_topic,
"messageId": messageId, # Example: "122e3e47835fefcd8aaf22d13ce21859"
"method": method, # Example: "GET",
"namespace": namespace, # Example: "Appliance.System.All",
"payloadVersion": 1,
"sign": signature, # Example: "b4236ac6fb399e70c3d61e98fcb68b74",
"timestamp": timestamp
},
"payload": payload
}
strdata = json.dumps(data)
return strdata.encode("utf-8"), messageId
def _generate_client_and_app_id(self):
md5_hash = md5()
rnd_uuid = UUID.uuid4()
md5_hash.update(("%s%s" % ("API", rnd_uuid)).encode("utf8"))
self._app_id = md5_hash.hexdigest()
self._client_id = 'app:%s' % md5_hash.hexdigest()

View File

@ -0,0 +1,9 @@
from enum import Enum
class ClientStatus(Enum):
INITIALIZED = 1
CONNECTING = 2
CONNECTED = 3
SUBSCRIBED = 4
CONNECTION_DROPPED = 5

View File

@ -0,0 +1,95 @@
import datetime
from threading import RLock, Condition
from meross_iot.cloud.client_status import ClientStatus
from meross_iot.cloud.exceptions.StatusTimeoutException import StatusTimeoutException
from meross_iot.cloud.timeouts import SHORT_TIMEOUT
from meross_iot.meross_event import ClientConnectionEvent
from meross_iot.logger import CONNECTION_MANAGER_LOGGER as l
class ConnectionStatusManager(object):
# The connection status of the device is represented by the following variable.
# It is protected by the following variable, called _client_connection_status_lock.
# The child classes should never change/access these variables directly, though.
_status = None
_lock = None
# List of callbacks that should be called when an event occurs
_connection_event_callbacks = None
_connection_event_callbacks_lock = None
# This condition object is used to synchronize multiple threads waiting for the connection to
# get into a specific state.
_status_condition = None
def __init__(self):
self._connection_event_callbacks_lock = RLock()
self._connection_event_callbacks = []
self._lock = RLock()
self._status_condition = Condition(self._lock)
self._status = ClientStatus.INITIALIZED
def get_status(self):
with self._status_condition:
return self._status
def update_status(self, status):
old_status = None
new_status = None
with self._status_condition:
old_status = self._status
new_status = status
self._status = status
self._status_condition.notify_all()
# If the connection status has changed, fire the event.
if old_status != new_status:
self._fire_connection_event(new_status)
def check_status(self, expected_status):
with self._lock:
return expected_status == self._status
def check_status_in(self, expected_statuses):
with self._lock:
return self._status in expected_statuses
def wait_for_status(self, expected_status, timeout=SHORT_TIMEOUT):
start_time = datetime.datetime.now()
with self._status_condition:
while self._status != expected_status:
elapsed = datetime.datetime.now().timestamp() - start_time.timestamp()
to = timeout - elapsed
timeout_hit = to < 0 or not self._status_condition.wait(to)
if timeout_hit:
# An error has occurred
raise StatusTimeoutException("Error while waiting for status %s. Last status is: %s" %
(expected_status, self._status))
# ------------------------------------------------------------------------------------------------
# Event Handling
# ------------------------------------------------------------------------------------------------
def register_connection_event_callback(self, callback):
with self._connection_event_callbacks_lock:
if callback not in self._connection_event_callbacks:
self._connection_event_callbacks.append(callback)
else:
l.debug("Callback was already registered.")
def unregister_connection_event_callback(self, callback):
with self._connection_event_callbacks_lock:
if callback in self._connection_event_callbacks:
self._connection_event_callbacks.remove(callback)
else:
l.debug("Callback was present: nothing to unregister.")
def _fire_connection_event(self, connection_status):
evt = ClientConnectionEvent(current_status=connection_status)
for c in self._connection_event_callbacks:
try:
c(evt)
except:
l.exception("Unhandled error occurred while executing event handler")

View File

@ -0,0 +1,187 @@
from abc import ABC, abstractmethod
from threading import RLock
from meross_iot.cloud.exceptions.OfflineDeviceException import OfflineDeviceException
from meross_iot.cloud.timeouts import LONG_TIMEOUT, SHORT_TIMEOUT
from meross_iot.cloud.abilities import *
from meross_iot.logger import DEVICE_LOGGER as l
from meross_iot.meross_event import DeviceOnlineStatusEvent
class AbstractMerossDevice(ABC):
# Device status + lock to protect concurrent access
_state_lock = None
online = False
# Device info and connection parameters
uuid = None
app_id = None
name = None
type = None
hwversion = None
fwversion = None
# Cached list of abilities
_abilities = None
# Cloud client: the object that handles mqtt communication with the Meross Cloud
__cloud_client = None
# Data structure for firing events.
__event_handlers_lock = None
__event_handlers = None
def __init__(self, cloud_client, device_uuid, **kwargs):
self.__cloud_client = cloud_client
self._state_lock = RLock()
self.__event_handlers_lock = RLock()
self.__event_handlers = []
self.uuid = device_uuid
if "channels" in kwargs:
self._channels = kwargs['channels']
# Information about device
if "devName" in kwargs:
self.name = kwargs['devName']
if "deviceType" in kwargs:
self.type = kwargs['deviceType']
if "fmwareVersion" in kwargs:
self.fwversion = kwargs['fmwareVersion']
if "hdwareVersion" in kwargs:
self.hwversion = kwargs['hdwareVersion']
if "onlineStatus" in kwargs:
self.online = kwargs['onlineStatus'] == 1
def handle_push_notification(self, namespace, payload, from_myself=False):
# Handle the ONLINE push notification
# Leave the rest to the specific implementation
if namespace == ONLINE:
old_online_status = self.online
status = payload['online']['status']
if status == 2:
with self._state_lock:
self.online = False
elif status == 1:
with self._state_lock:
self.online = True
else:
l.error("Unknown online status has been reported from the device: %d" % status)
# If the online status changed, fire the corresponding event
if old_online_status != self.online:
evt = DeviceOnlineStatusEvent(self, self.online)
self.fire_event(evt)
else:
self._handle_push_notification(namespace, payload, from_myself=from_myself)
def register_event_callback(self, callback):
with self.__event_handlers_lock:
if callback not in self.__event_handlers:
self.__event_handlers.append(callback)
else:
l.debug("The callback you tried to register is already present.")
pass
def unregister_event_callback(self, callback):
with self.__event_handlers_lock:
if callback in self.__event_handlers:
self.__event_handlers.remove(callback)
else:
l.debug("The callback you tried to unregister is not present.")
pass
def fire_event(self, eventobj):
for c in self.__event_handlers:
try:
c(eventobj)
except:
l.exception("Unhandled error occurred while executing the registered event-callback")
@abstractmethod
def _handle_push_notification(self, namespace, payload, from_myself=False):
"""
Handles push messages for this device. This method should be implemented by the base class in order
to catch status changes issued by other clients (i.e. the Meross app on the user's device).
:param namespace:
:param message:
:param from_myself: boolean flag. When true, it means that the notification is generated in response to a
command that was issued by this client. When false, it means that another client generated the event.
:return:
"""
pass
@abstractmethod
def get_status(self):
pass
def execute_command(self, command, namespace, payload, callback=None, timeout=SHORT_TIMEOUT):
with self._state_lock:
# If the device is not online, what's the point of issuing the command?
if not self.online:
raise OfflineDeviceException("The device %s (%s) is offline. The command cannot be executed" %
(self.name, self.uuid))
return self.__cloud_client.execute_cmd(self.uuid, command, namespace, payload, callback=callback, timeout=timeout)
def get_sys_data(self):
return self.execute_command("GET", ALL, {})
def get_abilities(self):
# TODO: Make this cached value expire after a bit...
if self._abilities is None:
self._abilities = self.execute_command("GET", ABILITY, {})['ability']
return self._abilities
def get_report(self):
return self.execute_command("GET", REPORT, {})
def get_wifi_list(self):
if WIFI_LIST in self.get_abilities():
return self.execute_command("GET", WIFI_LIST, {}, timeout=LONG_TIMEOUT)
else:
l.error("This device does not support the WIFI_LIST ability")
return None
def get_trace(self):
if TRACE in self.get_abilities():
return self.execute_command("GET", TRACE, {})
else:
l.error("This device does not support the TRACE ability")
return None
def get_debug(self):
if DEBUG in self.get_abilities():
return self.execute_command("GET", DEBUG, {})
else:
l.error("This device does not support the DEBUG ability")
return None
def supports_consumption_reading(self):
return CONSUMPTIONX in self.get_abilities()
def supports_electricity_reading(self):
return ELECTRICITY in self.get_abilities()
def supports_light_control(self):
return LIGHT in self.get_abilities()
@abstractmethod
def get_power_consumption(self):
pass
@abstractmethod
def get_electricity(self):
pass
def __str__(self):
basic_info = "%s: %s (%s, HW %s, FW %s): " % (
self.__class__.name,
self.name,
self.type,
self.hwversion,
self.fwversion
)
return basic_info

View File

@ -0,0 +1,19 @@
from meross_iot.cloud.devices.power_plugs import GenericPlug
from meross_iot.cloud.devices.light_bulbs import GenericBulb
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
def build_wrapper(
cloud_client,
device_type, # type: str
device_uuid, # type: str
device_specs # type: dict
):
if device_type.startswith('msl') or device_type.startswith('mss560m'):
return GenericBulb(cloud_client, device_uuid=device_uuid, **device_specs)
elif device_type.startswith('mss'):
return GenericPlug(cloud_client, device_uuid=device_uuid, **device_specs)
elif device_type.startswith('msg'):
return GenericGarageDoorOpener(cloud_client, device_uuid=device_uuid, **device_specs)
else:
return GenericPlug(cloud_client, device_uuid=device_uuid, **device_specs)

View File

@ -0,0 +1,173 @@
from meross_iot.cloud.abilities import *
from meross_iot.cloud.device import AbstractMerossDevice
from meross_iot.logger import POWER_PLUGS_LOGGER as l
from meross_iot.meross_event import DeviceDoorStatusEvent
from threading import Event
def parse_state(state):
if isinstance(state, str):
strstate = state.strip().lower()
if strstate == 'open':
return True
elif strstate == 'closed':
return False
else:
raise ValueError("Invalid state provided.")
elif isinstance(state, bool):
return state
elif isinstance(state, int):
if state == 0:
return False
elif state == 1:
return True
else:
return ValueError("Invalid state provided.")
def compare_same_states(state1, state2):
s1 = parse_state(state1)
s2 = parse_state(state2)
return s1 == s2
class GenericGarageDoorOpener(AbstractMerossDevice):
# Channels
_channels = []
# Dictionary {channel_id (door) -> status}
_door_state = None
def __init__(self, cloud_client, device_uuid, **kwords):
super(GenericGarageDoorOpener, self).__init__(cloud_client, device_uuid, **kwords)
def get_status(self):
with self._state_lock:
if self._door_state is None:
self._get_status_impl()
return self._door_state
def _handle_push_notification(self, namespace, payload, from_myself=False):
def fire_garage_door_state_change(dev, channel_id, o_state, n_state, f_myself):
if o_state != n_state:
evt = DeviceDoorStatusEvent(dev=dev, channel_id=channel_id, door_state=n_state,
generated_by_myself=f_myself)
self.fire_event(evt)
with self._state_lock:
if namespace == GARAGE_DOOR_STATE:
for door in payload['state']:
channel_index = door['channel']
state = door['open'] == 1
old_state = self._door_state[channel_index]
self._door_state[channel_index] = state
fire_garage_door_state_change(self, channel_index, old_state, state, from_myself)
elif namespace == REPORT:
# For now, we simply ignore push notification of these kind.
# In the future, we might think of handling such notification by caching them
# and avoid the network round-trip when asking for power consumption (if the latest report is
# recent enough)
pass
else:
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
def _get_status_impl(self):
if self._door_state is None:
self._door_state = {}
data = self.get_sys_data()['all']
if 'digest' in data:
for c in data['digest']['garageDoor']:
self._door_state[c['channel']] = c['open'] == 1
return self._door_state
def _get_channel_id(self, channel):
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
if channel in self._channels:
return self._channels.index(channel)
# if a channel name is given, lookup the channel id from the name
if isinstance(channel, str):
for i, c in enumerate(self.get_channels()):
if c['devName'] == channel:
return c['channel']
# If an integer is given assume that is the channel ID
elif isinstance(channel, int):
return channel
# In other cases return an error
raise Exception("Invalid channel specified.")
def _operate_door(self, channel, state, callback, wait_for_sensor_confirmation):
# If the door is already in the target status, do not execute the command.
already_in_state = False
with self._state_lock:
already_in_state = self.get_status()[channel] == state
if already_in_state and callback is None:
l.info("Command was not executed: the door state is already %s" % ("open" if state else "closed"))
return
elif already_in_state and callback is not None:
callback(None, self._door_state[channel])
return
payload = {"state": {"channel": channel, "open": state, "uuid": self.uuid}}
if wait_for_sensor_confirmation:
door_event = None
if callback is None:
door_event = Event()
def waiter(data):
self.unregister_event_callback(waiter)
if data.channel != channel:
return
if callback is None:
door_event.set()
else:
if not compare_same_states(data.door_state, state):
callback("Operation failed", data.door_state)
else:
callback(None, data.door_state)
self.register_event_callback(waiter)
self.execute_command(command="SET", namespace=GARAGE_DOOR_STATE, payload=payload, callback=None)
if callback is None:
door_event.wait()
current_state = self._door_state[channel]
if current_state != state:
raise Exception("Operation failed.")
else:
self.execute_command(command="SET", namespace=GARAGE_DOOR_STATE, payload=payload, callback=callback)
def open_door(self, channel=0, callback=None, ensure_opened=True):
c = self._get_channel_id(channel)
return self._operate_door(c, 1, callback=callback, wait_for_sensor_confirmation=ensure_opened)
def close_door(self, channel=0, callback=None, ensure_closed=True):
c = self._get_channel_id(channel)
return self._operate_door(c, 0, callback=callback, wait_for_sensor_confirmation=ensure_closed)
def get_channels(self):
return self._channels
def get_power_consumption(self):
return None
def get_electricity(self):
return None
def __str__(self):
base_str = super().__str__()
with self._state_lock:
if not self.online:
return base_str
doors = "Doors -> "
doors += ",".join(["%d = %s" % (k, "OPEN" if v else "CLOSED") for k, v in enumerate(self.get_status())])
return base_str + doors

View File

@ -0,0 +1,247 @@
from meross_iot.cloud.abilities import *
from meross_iot.cloud.device import AbstractMerossDevice
from meross_iot.logger import BULBS_LOGGER as l
from meross_iot.meross_event import BulbSwitchStateChangeEvent, BulbLightStateChangeEvent
def to_rgb(rgb):
if rgb is None:
return None
elif isinstance(rgb, int):
return rgb
elif isinstance(rgb, tuple):
red, green, blue = rgb
elif isinstance(rgb, dict):
red = rgb['red']
green = rgb['green']
blue = rgb['blue']
else:
raise Exception("Invalid value for RGB!")
r = red << 16
g = green << 8
b = blue
return r+g+b
class GenericBulb(AbstractMerossDevice):
# Bulb state: dictionary of channel-id/bulb-state
_state = None
def __init__(self, cloud_client, device_uuid, **kwords):
self._state = {}
super(GenericBulb, self).__init__(cloud_client, device_uuid, **kwords)
def _channel_control_impl(self, channel, status):
if TOGGLE in self.get_abilities():
return self._toggle(status)
elif TOGGLEX in self.get_abilities():
return self._togglex(channel, status)
else:
raise Exception("The current device does not support neither TOGGLE nor TOGGLEX.")
def _update_state(self, channel, **kwargs):
with self._state_lock:
if channel not in self._state:
self._state[channel] = {}
for k in kwargs:
if k == 'onoff':
self._state[channel]['onoff'] = kwargs[k]
elif kwargs[k] is not None:
self._state[channel][k] = kwargs[k]
def _toggle(self, status):
payload = {"channel": 0, "toggle": {"onoff": status}}
return self.execute_command("SET", TOGGLE, payload)
def _togglex(self, channel, status):
payload = {'togglex': {"onoff": status, "channel": channel}}
return self.execute_command("SET", TOGGLEX, payload)
def _handle_push_notification(self, namespace, payload, from_myself=False):
def fire_bulb_switch_state_change(dev, channel_id, o_state, n_state, f_myself):
if o_state != n_state:
evt = BulbSwitchStateChangeEvent(dev=dev, channel_id=channel_id, is_on=n_state,
generated_by_myself=f_myself)
self.fire_event(evt)
def fire_bulb_light_state_change(dev, channel_id, o_state, n_state, f_myself):
if o_state != n_state:
evt = BulbLightStateChangeEvent(dev=dev, channel_id=channel_id, light_state=n_state,
generated_by_myself=f_myself)
self.fire_event(evt)
with self._state_lock:
if namespace == TOGGLE:
on_status=payload['toggle']['onoff'] == 1
channel_index = 0
old_state = self._state.get(channel_index)
new_state = on_status
self._update_state(channel=0, onoff=on_status)
fire_bulb_switch_state_change(self, channel_id=0, o_state=old_state, n_state=new_state,
f_myself=from_myself)
elif namespace == TOGGLEX:
if isinstance(payload['togglex'], list):
for c in payload['togglex']:
channel_index = c['channel']
on_status = c['onoff'] == 1
old_state = self._state.get(channel_index)
if old_state is not None:
old_state = old_state.get('onoff')
self._update_state(channel=channel_index, onoff=on_status)
fire_bulb_switch_state_change(self, channel_id=channel_index, o_state=old_state,
n_state=on_status, f_myself=from_myself)
elif isinstance(payload['togglex'], dict):
channel_index = payload['togglex']['channel']
on_status = payload['togglex']['onoff'] == 1
old_state = self._state.get(channel_index).get('onoff')
if old_state is not None:
old_state = old_state.get('onoff')
self._update_state(channel=channel_index, onoff=on_status)
fire_bulb_switch_state_change(self, channel_id=channel_index, o_state=old_state, n_state=on_status,
f_myself=from_myself)
elif namespace == LIGHT:
channel_index = payload['light']['channel']
old_state = self._state.get(channel_index)
new_state = payload['light']
del new_state['channel']
self._update_state(channel=channel_index, **new_state)
fire_bulb_light_state_change(self, channel_id=channel_index, o_state=old_state, n_state=new_state,
f_myself=from_myself)
elif namespace == REPORT:
# For now, we simply ignore push notification of these kind.
# In the future, we might think of handling such notification by caching them
# and avoid the network round-trip when asking for power consumption (if the latest report is
# recent enough)
pass
else:
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
def _get_status_impl(self):
res = {}
data = self.get_sys_data()['all']
if 'digest' in data:
light_channel = data['digest']['light']['channel']
res[light_channel] = data['digest']['light']
for c in data['digest']['togglex']:
res[c['channel']]['onoff'] = c['onoff'] == 1
elif 'control' in data:
res[0]['onoff'] = data['control']['toggle']['onoff'] == 1
return res
def _get_channel_id(self, channel):
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
if channel in self._channels:
return self._channels.index(channel)
# if a channel name is given, lookup the channel id from the name
if isinstance(channel, str):
for i, c in enumerate(self.get_channels()):
if c['devName'] == channel:
return c['channel']
# If an integer is given assume that is the channel ID
elif isinstance(channel, int):
return channel
# In other cases return an error
raise Exception("Invalid channel specified.")
def get_status(self, channel=0):
# In order to optimize the network traffic, we don't call the get_status() api at every request.
# On the contrary, we call it the first time. Then, the rest of the API will silently listen
# for state changes and will automatically update the self._state structure listening for
# messages of the device.
# Such approach, however, has a side effect. If we call TOGGLE/TOGGLEX and immediately after we call
# get_status(), the reported status will be still the old one. This is a race condition because the
# "status" RESPONSE will be delivered some time after the TOGGLE REQUEST. It's not a big issue for now,
# and synchronizing the two things would be inefficient and probably not very useful.
# Just remember to wait some time before testing the status of the item after a toggle.
c = self._get_channel_id(channel)
if self._state == {}:
current_state = self._get_status_impl()
with self._state_lock:
self._state = current_state
return self._state[c]
def get_channels(self):
return self._channels
def get_channel_status(self, channel):
ch_id = self._get_channel_id(channel)
c = self._get_channel_id(ch_id)
return self.get_status(c)
def turn_on_channel(self, channel):
ch_id = self._get_channel_id(channel)
c = self._get_channel_id(ch_id)
return self._channel_control_impl(c, 1)
def turn_off_channel(self, channel):
ch_id = self._get_channel_id(channel)
c = self._get_channel_id(ch_id)
return self._channel_control_impl(c, 0)
def turn_on(self, channel=0):
ch_id = self._get_channel_id(channel)
c = self._get_channel_id(ch_id)
return self._channel_control_impl(c, 1)
def turn_off(self, channel=0):
ch_id = self._get_channel_id(channel)
c = self._get_channel_id(ch_id)
return self._channel_control_impl(c, 0)
def set_light_color(self, channel=0, rgb=None, luminance=100, temperature=100, capacity=5):
ch_id = self._get_channel_id(channel)
# Convert the RGB to integer
color = to_rgb(rgb)
payload = {
'light': {
'capacity': capacity,
'channel': ch_id,
'gradual': 0,
'luminance': luminance,
'rgb': color,
'temperature': temperature
}
}
# TODO: fix this as soon as we get hands on a real MSS560 and see what the payload looks like...
# handle mss560m differently
if self.type.lower() == 'mss560m':
pl = {
'light': self.get_light_color()
}
pl['light']['channel'] = channel
pl['light']['luminance'] = luminance
payload = pl
self.execute_command(command='SET', namespace=LIGHT, payload=payload)
def get_light_color(self, channel=0):
ch_id = self._get_channel_id(channel)
return self.get_status(channel=ch_id)
def get_power_consumption(self):
return None
def get_electricity(self):
return None
def __str__(self):
base_str = super().__str__()
with self._state_lock:
if not self.online:
return base_str
channels = "Channels: "
channels += ",".join(["%d = %s" % (k, "ON" if v else "OFF") for k, v in enumerate(self._state)])
return base_str + "\n" + "\n" + channels

View File

@ -0,0 +1,199 @@
from meross_iot.cloud.abilities import *
from meross_iot.cloud.device import AbstractMerossDevice
from meross_iot.logger import POWER_PLUGS_LOGGER as l
from meross_iot.meross_event import DeviceSwitchStatusEvent
class GenericPlug(AbstractMerossDevice):
# Channels
_channels = []
# Dictionary {channel->status}
_state = {}
def __init__(self, cloud_client, device_uuid, **kwords):
super(GenericPlug, self).__init__(cloud_client, device_uuid, **kwords)
def _get_consumptionx(self):
return self.execute_command("GET", CONSUMPTIONX, {})
def _get_electricity(self):
return self.execute_command("GET", ELECTRICITY, {})
def _toggle(self, status, callback=None):
payload = {"channel": 0, "toggle": {"onoff": status}}
return self.execute_command("SET", TOGGLE, payload, callback=callback)
def _togglex(self, channel, status, callback=None):
payload = {'togglex': {"onoff": status, "channel": channel}}
return self.execute_command("SET", TOGGLEX, payload, callback=callback)
def _channel_control_impl(self, channel, status, callback=None):
if TOGGLE in self.get_abilities():
return self._toggle(status, callback=callback)
elif TOGGLEX in self.get_abilities():
return self._togglex(channel, status, callback=callback)
else:
raise Exception("The current device does not support neither TOGGLE nor TOGGLEX.")
def _handle_push_notification(self, namespace, payload, from_myself=False):
def fire_switch_state_change(dev, channel_id, o_state, n_state, f_myself):
if o_state != n_state:
evt = DeviceSwitchStatusEvent(dev=dev, channel_id=channel_id, switch_state=n_state,
generated_by_myself=f_myself)
self.fire_event(evt)
with self._state_lock:
if namespace == TOGGLE:
# Update the local state and fire the event only if the state actually changed
channel_index = 0
old_switch_state = self._state.get(channel_index)
switch_state = payload['toggle']['onoff'] == 1
self._state[channel_index] = switch_state
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
elif namespace == TOGGLEX:
if isinstance(payload['togglex'], list):
for c in payload['togglex']:
# Update the local state and fire the event only if the state actually changed
channel_index = c['channel']
old_switch_state = self._state.get(channel_index)
switch_state = c['onoff'] == 1
self._state[channel_index] = switch_state
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
elif isinstance(payload['togglex'], dict):
# Update the local state and fire the event only if the state actually changed
channel_index = payload['togglex']['channel']
old_switch_state = self._state.get(channel_index)
switch_state = payload['togglex']['onoff'] == 1
self._state[channel_index] = switch_state
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
elif namespace == REPORT or namespace == CONSUMPTIONX:
# For now, we simply ignore push notification of these kind.
# In the future, we might think of handling such notification by caching them
# and avoid the network round-trip when asking for power consumption (if the latest report is
# recent enough)
pass
else:
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
def _get_status_impl(self):
res = {}
data = self.get_sys_data()['all']
if 'digest' in data:
for c in data['digest']['togglex']:
res[c['channel']] = c['onoff'] == 1
elif 'control' in data:
res[0] = data['control']['toggle']['onoff'] == 1
return res
def _get_channel_id(self, channel):
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
if channel in self._channels:
return self._channels.index(channel)
# if a channel name is given, lookup the channel id from the name
if isinstance(channel, str):
for i, c in enumerate(self.get_channels()):
if c['devName'] == channel:
return c['channel']
# If an integer is given assume that is the channel ID
elif isinstance(channel, int):
return channel
# In other cases return an error
raise Exception("Invalid channel specified.")
def get_status(self, channel=0):
# In order to optimize the network traffic, we don't call the get_status() api at every request.
# On the contrary, we only call it the first time. Then, the rest of the API will silently listen
# for state changes and will automatically update the self._state structure listening for
# messages of the device.
# Such approach, however, has a side effect. If we call TOGGLE/TOGGLEX and immediately after we call
# get_status(), the reported status will be still the old one. This is a race condition because the
# "status" RESPONSE will be delivered some time after the TOGGLE REQUEST. It's not a big issue for now,
# and synchronizing the two things would be inefficient and probably not very useful.
# Just remember to wait some time before testing the status of the item after a toggle.
with self._state_lock:
c = self._get_channel_id(channel)
if self._state == {}:
self._state = self._get_status_impl()
return self._state[c]
def get_power_consumption(self):
if CONSUMPTIONX in self.get_abilities():
return self._get_consumptionx()['consumptionx']
else:
# Not supported!
return None
def get_electricity(self):
if ELECTRICITY in self.get_abilities():
return self._get_electricity()['electricity']
else:
# Not supported!
return None
def get_channels(self):
return self._channels
def get_channel_status(self, channel):
c = self._get_channel_id(channel)
return self.get_status(c)
def turn_on_channel(self, channel, callback=None):
c = self._get_channel_id(channel)
return self._channel_control_impl(c, 1, callback=callback)
def turn_off_channel(self, channel, callback=None):
c = self._get_channel_id(channel)
return self._channel_control_impl(c, 0, callback=callback)
def turn_on(self, channel=0, callback=None):
c = self._get_channel_id(channel)
return self._channel_control_impl(c, 1, callback=callback)
def turn_off(self, channel=0, callback=None):
c = self._get_channel_id(channel)
return self._channel_control_impl(c, 0, callback=callback)
def get_usb_channel_index(self):
# Look for the usb channel
for i, c in enumerate(self.get_channels()):
if 'type' in c and c['type'] == 'USB':
return i
return None
def enable_usb(self, callback=None):
c = self.get_usb_channel_index()
if c is None:
return
else:
return self.turn_on_channel(c, callback=callback)
def disable_usb(self, callback=None):
c = self.get_usb_channel_index()
if c is None:
return
else:
return self.turn_off_channel(c, callback=callback)
def get_usb_status(self):
c = self.get_usb_channel_index()
if c is None:
return
else:
return self.get_channel_status(c)
def __str__(self):
base_str = super().__str__()
with self._state_lock:
if not self.online:
return base_str
channels = "Channels: "
channels += ",".join(["%d = %s" % (k, "ON" if v else "OFF") for k, v in enumerate(self._state)])
return base_str + "\n" + "\n" + channels

View File

@ -0,0 +1,2 @@
class CommandTimeoutException(Exception):
pass

View File

@ -0,0 +1,2 @@
class ConnectionDroppedException(Exception):
pass

View File

@ -0,0 +1,2 @@
class OfflineDeviceException(Exception):
pass

View File

@ -0,0 +1,2 @@
class StatusTimeoutException(Exception):
pass

View File

@ -0,0 +1,2 @@
LONG_TIMEOUT = 30.0 # For wifi scan
SHORT_TIMEOUT = 10.0 # For any other command

View File

@ -0,0 +1,5 @@
class MerossCloudCreds(object):
token = None
key = None
user_id = None
user_email = None

View File

@ -0,0 +1,24 @@
import logging
from logging import StreamHandler
from sys import stdout
ROOT_MEROSS_LOGGER = logging.getLogger("meross")
MANAGER_LOGGER = ROOT_MEROSS_LOGGER.getChild("manager")
CONNECTION_MANAGER_LOGGER = ROOT_MEROSS_LOGGER.getChild("connection")
NETWORK_DATA = ROOT_MEROSS_LOGGER.getChild("network_data")
POWER_PLUGS_LOGGER = ROOT_MEROSS_LOGGER.getChild("power_plugs")
BULBS_LOGGER = ROOT_MEROSS_LOGGER.getChild("light_bulbs")
DEVICE_LOGGER = ROOT_MEROSS_LOGGER.getChild("generic_device")
h = StreamHandler(stream=stdout)
ROOT_MEROSS_LOGGER.addHandler(h)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
h.setFormatter(formatter)
# Call this module to adjust the verbosity of the stream output. By default, only INFO is written to STDOUT log.
def set_log_level(root=logging.DEBUG, connection=logging.INFO, network=logging.INFO):
ROOT_MEROSS_LOGGER.setLevel(root)
CONNECTION_MANAGER_LOGGER.setLevel(connection)
NETWORK_DATA.setLevel(network)

View File

@ -0,0 +1,177 @@
from meross_iot.api import MerossHttpClient
from meross_iot.cloud.client import MerossCloudClient
from meross_iot.cloud.device_factory import build_wrapper
from threading import RLock
from meross_iot.logger import MANAGER_LOGGER as l
from meross_iot.meross_event import DeviceOnlineStatusEvent
class MerossManager(object):
# HTTPClient object used to discover devices
_http_client = None
# Dictionary of devices that are currently handled by this manager
# as UUID -> Device
_devices = None
# Lock item used to protect access to the device collection
_devices_lock = None
# Cloud credentials to be used against the Meross MQTT cloud
_cloud_creds = None
_cloud_client = None
# List of callbacks that should be called when an event occurs
_event_callbacks = None
_event_callbacks_lock = None
def __init__(self, meross_email, meross_password):
self._devices_lock = RLock()
self._devices = dict()
self._event_callbacks_lock = RLock()
self._event_callbacks = []
self._http_client = MerossHttpClient(email=meross_email, password=meross_password)
self._cloud_creds = self._http_client.get_cloud_credentials()
# Instantiate the mqtt cloud client
self._cloud_client = MerossCloudClient(cloud_credentials=self._cloud_creds,
push_message_callback=self._dispatch_push_notification)
self._cloud_client.connection_status.register_connection_event_callback(callback=self._fire_event)
def start(self):
# Connect to the mqtt broker
self._cloud_client.connect()
self._discover_devices()
def stop(self):
self._cloud_client.close()
def register_event_handler(self, callback):
with self._event_callbacks_lock:
if callback in self._event_callbacks:
pass
else:
self._event_callbacks.append(callback)
def unregister_event_handler(self, callback):
with self._event_callbacks_lock:
if callback not in self._event_callbacks:
pass
else:
self._event_callbacks.remove(callback)
def get_device_by_uuid(self, uuid):
dev = None
with self._devices_lock:
dev = self._devices.get(uuid)
return dev
def get_device_by_name(self, name):
with self._devices_lock:
for k, v in self._devices.items():
if v.name.lower() == name.lower():
return v
return None
def get_supported_devices(self):
return [x for k, x in self._devices.items()]
def get_devices_by_kind(self, clazz):
res = []
with self._devices_lock:
for k, v in self._devices.items():
if isinstance(v, clazz):
res.append(v)
return res
def get_devices_by_type(self, type_name):
res = []
with self._devices_lock:
for k, v in self._devices.items():
if v.type.lower() == type_name.lower():
res.append(v)
return res
def _dispatch_push_notification(self, message, from_myself=False):
"""
When a push notification is received from the MQTT client, it needs to be delivered to the
corresponding device. This method serves that scope.
:param message:
:param from_myself: boolean flag. When True, it means that the message received is related to a
previous request issued by this client. When is false, it means the message is related to some other
client.
:return:
"""
header = message['header'] # type: dict
payload = message['payload'] # type: dict
# Identify the UUID of the target device by looking at the FROM field of the message header
dev_uuid = header['from'].split('/')[2]
device = None
with self._devices_lock:
device = self._devices.get(dev_uuid)
if device is not None:
namespace = header['namespace']
device.handle_push_notification(namespace, payload, from_myself=from_myself)
else:
# If we receive a push notification from a device that is not yet contained into our registry,
# it probably means a new one has just been registered with the meross cloud.
# Therefor, let's retrieve info from the HTTP api.
self._discover_devices()
def _discover_devices(self, online_only=False):
"""
Discovers the devices that are visible via HTTP API and update the internal list of
managed devices accordingly.
:return:
"""
for dev in self._http_client.list_devices():
online = dev['onlineStatus']
if online_only and online != 1:
# The device is not online, so we skip it.
continue
# If the device we have discovered is not in the list we already handle, we need to add it.
self._handle_device_discovered(dev)
return self._devices
def _handle_device_discovered(self, dev):
d_type = dev['deviceType']
d_uuid = dev['uuid']
device = build_wrapper(device_type=d_type, device_uuid=d_uuid, cloud_client=self._cloud_client,
device_specs=dev)
if device is not None:
# Check if the discovered device is already in the list of handled devices.
# If not, add it right away. Otherwise, ignore it.
is_new = False
new_dev = None
with self._devices_lock:
if d_uuid not in self._devices:
is_new = True
new_dev = build_wrapper(cloud_client=self._cloud_client,
device_type=d_type, device_uuid=d_uuid, device_specs=dev)
self._devices[d_uuid] = new_dev
# If this is new device, register the event handler for it and fire the ONLINE event.
if is_new:
with self._event_callbacks_lock:
for c in self._event_callbacks:
new_dev.register_event_callback(c)
evt = DeviceOnlineStatusEvent(new_dev, new_dev.online)
self._fire_event(evt)
def _fire_event(self, eventobj):
for c in self._event_callbacks:
try:
c(eventobj)
except:
l.exception("An unhandled error occurred while invoking callback")

View File

@ -0,0 +1,103 @@
from enum import Enum
class MerossEventType(Enum):
# Fired when the MQTT client connects/disconnects to the MQTT broker
CLIENT_CONNECTION = 10
DEVICE_ONLINE_STATUS = 100
DEVICE_SWITCH_STATUS = 1000
DEVICE_BULB_SWITCH_STATE = 2000
DEVICE_BULB_STATE = 2001
GARAGE_DOOR_STATUS = 2000
class MerossEvent(object):
event_type = None # type: MerossEventType
def __init__(self, event_type):
self.event_type = event_type
class ClientConnectionEvent(MerossEvent):
status = None
def __init__(self, current_status):
super(ClientConnectionEvent, self).__init__(MerossEventType.CLIENT_CONNECTION)
self.status = current_status
class DeviceOnlineStatusEvent(MerossEvent):
# Pointer to the device object
device = None
# Current status of the device
status = None
def __init__(self, dev, current_status):
super(DeviceOnlineStatusEvent, self).__init__(MerossEventType.DEVICE_ONLINE_STATUS)
self.device = dev
self.status = "online" if current_status else "offline"
class DeviceSwitchStatusEvent(MerossEvent):
# Pointer to the device object
device = None
# Channel ID where the event occurred
channel_id = None
# Current state of the switch where the event occurred
switch_state = None
# Indicates id the event was generated by a command issued by the library itself.
# This is particularly useful in the case the user handler wants only to react
# to events generated by third parties.
generated_by_myself = None
def __init__(self, dev, channel_id, switch_state, generated_by_myself):
super(DeviceSwitchStatusEvent, self).__init__(MerossEventType.DEVICE_SWITCH_STATUS)
self.device = dev
self.channel_id = channel_id
self.switch_state = switch_state
self.generated_by_myself = generated_by_myself
class DeviceDoorStatusEvent(MerossEvent):
# Pointer to the device object
device = None
# Current state of the door
door_state = None
# Channel related to the door controller
channel = None
# Indicates id the event was generated by a command issued by the library itself.
# This is particularly useful in the case the user handler wants only to react
# to events generated by third parties.
generated_by_myself = None
def __init__(self, dev, channel_id, door_state, generated_by_myself):
super(DeviceDoorStatusEvent, self).__init__(MerossEventType.GARAGE_DOOR_STATUS)
self.device = dev
self.channel = channel_id
self.door_state = "open" if door_state else "closed"
self.generated_by_myself = generated_by_myself
class BulbSwitchStateChangeEvent(MerossEvent):
def __init__(self, dev, channel_id, is_on, generated_by_myself):
super(BulbSwitchStateChangeEvent, self).__init__(MerossEventType.DEVICE_BULB_SWITCH_STATE)
self.device = dev
self.channel = channel_id
self.is_on = is_on
self.generated_by_myself = generated_by_myself
class BulbLightStateChangeEvent(MerossEvent):
def __init__(self, dev, channel_id, light_state, generated_by_myself):
super(BulbLightStateChangeEvent, self).__init__(MerossEventType.DEVICE_BULB_STATE)
self.device = dev
self.channel = channel_id
self.light_state = light_state
self.generated_by_myself = generated_by_myself

View File

@ -0,0 +1,23 @@
from threading import RLock
class AtomicCounter(object):
_lock = None
def __init__(self, initialValue):
self._lock = RLock()
self._val = initialValue
def dec(self):
with self._lock:
self._val -= 1
return self._val
def inc(self):
with self._lock:
self._val += 1
return self._val
def get(self):
with self._lock:
return self._val

View File

@ -0,0 +1,3 @@
paho-mqtt>=1.3.1
requests>=2.19.1
retrying>=1.3.3

View File

@ -0,0 +1,43 @@
from os import path
from setuptools import setup, find_packages
here = path.abspath(path.dirname(__file__))
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
setup(
name='meross_iot',
version='0.3.1.3',
packages=find_packages(exclude=('tests',)),
url='https://github.com/albertogeniola/MerossIot',
license='MIT',
author='Alberto Geniola',
author_email='albertogeniola@gmail.com',
classifiers=[
'Intended Audience :: Developers',
'Programming Language :: Python :: 3',
'Operating System :: OS Independent'
],
description='A simple library to deal with Meross devices. At the moment MSS110, MSS210, MSS310, MSS310H '
'smart plugs and the MSS425E power strip. Other meross device might work out of the box with limited '
'functionality. Give it a try and, in case of problems, let the developer know by opening an issue '
'on Github.',
long_description=long_description,
long_description_content_type='text/markdown',
keywords='meross smartplug smartbulb iot mqtt domotic switch mss310 mss210 mss110 mss425e msl20 msg100',
project_urls={
'Documentation': 'https://github.com/albertogeniola/MerossIot',
'Funding': 'https://donate.pypi.org',
'Source': 'https://github.com/albertogeniola/MerossIot',
'Tracker': 'https://github.com/albertogeniola/MerossIot/issues',
},
install_requires=[
'paho-mqtt>=1.3.1',
'requests>=2.19.1',
'retrying>=1.3.3',
],
python_requires='>=3.5',
test_suite='tests'
)

View File

@ -0,0 +1,143 @@
from meross_iot.manager import MerossManager
from meross_iot.meross_event import MerossEventType
from meross_iot.cloud.devices.light_bulbs import GenericBulb
from meross_iot.cloud.devices.power_plugs import GenericPlug
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
import time
import os
EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
def event_handler(eventobj):
if eventobj.event_type == MerossEventType.DEVICE_ONLINE_STATUS:
print("Device online status changed: %s went %s" % (eventobj.device.name, eventobj.status))
pass
elif eventobj.event_type == MerossEventType.DEVICE_SWITCH_STATUS:
print("Switch state changed: Device %s (channel %d) went %s" % (eventobj.device.name, eventobj.channel_id,
eventobj.switch_state))
elif eventobj.event_type == MerossEventType.CLIENT_CONNECTION:
print("MQTT connection state changed: client went %s" % eventobj.status)
# TODO: Give example of reconnection?
elif eventobj.event_type == MerossEventType.GARAGE_DOOR_STATUS:
print("Garage door is now %s" % eventobj.door_state)
else:
print("Unknown event!")
if __name__ == '__main__':
# Initiates the Meross Cloud Manager. This is in charge of handling the communication with the remote endpoint
manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
# Register event handlers for the manager...
manager.register_event_handler(event_handler)
# Starts the manager
manager.start()
# You can retrieve the device you are looking for in various ways:
# By kind
bulbs = manager.get_devices_by_kind(GenericBulb)
plugs = manager.get_devices_by_kind(GenericPlug)
door_openers = manager.get_devices_by_kind(GenericGarageDoorOpener)
all_devices = manager.get_supported_devices()
# Print some basic specs about the discovered devices
print("All the bulbs I found:")
for b in bulbs:
print(b)
print("All the plugs I found:")
for p in plugs:
print(p)
print("All the garage openers I found:")
for g in door_openers:
print(g)
print("All the supported devices I found:")
for d in all_devices:
print(d)
# You can also retrieve devices by the UUID/name
# a_device = manager.get_device_by_name("My Plug")
# a_device = manager.get_device_by_uuid("My Plug")
# Or you can retrieve all the device by the HW type
# all_mss310 = manager.get_devices_by_type("mss310")
# ------------------------------
# Let's play the garage openers.
# ------------------------------
for g in door_openers:
if not g.online:
print("The garage controller %s seems to be offline. Cannot play with that..." % g.name)
continue
print("Opening door %s..." % g.name)
g.open_door()
print("Closing door %s..." % g.name)
g.close_door()
# ---------------------
# Let's play with bulbs
# ---------------------
for b in bulbs: # type: GenericBulb
if not b.online:
print("The bulb %s seems to be offline. Cannot play with that..." % b.name)
continue
print("Let's play with bulb %s" % b.name)
if not b.supports_light_control():
print("Too bad bulb %s does not support light control %s" % b.name)
else:
# Let's make it red!
b.set_light_color(rgb=(255, 0, 0))
b.turn_on()
time.sleep(1)
b.turn_off()
# ---------------------------
# Let's play with smart plugs
# ---------------------------
for p in plugs: # type: GenericPlug
if not p.online:
print("The plug %s seems to be offline. Cannot play with that..." % p.name)
continue
print("Let's play with smart plug %s" % p.name)
channels = len(p.get_channels())
print("The plug %s supports %d channels." % (p.name, channels))
for i in range(0, channels):
print("Turning on channel %d of %s" % (i, p.name))
p.turn_on_channel(i)
time.sleep(1)
print("Turning off channel %d of %s" % (i, p.name))
p.turn_off_channel(i)
usb_channel = p.get_usb_channel_index()
if usb_channel is not None:
print("Awesome! This device also supports USB power.")
p.enable_usb()
time.sleep(1)
p.disable_usb()
if p.supports_electricity_reading():
print("Awesome! This device also supports power consumption reading.")
print("Current consumption is: %s" % str(p.get_electricity()))
# At this point, we are all done playing with the library, so we gracefully disconnect and clean resources.
print("We are done playing. Cleaning resources...")
manager.stop()
print("Bye bye!")

View File

@ -0,0 +1,61 @@
import os
import time
import unittest
from meross_iot.manager import MerossManager
from threading import Thread, current_thread
import random
from meross_iot.logger import set_log_level
from logging import DEBUG, INFO
from meross_iot.utilities.synchronization import AtomicCounter
EMAIL = os.environ.get('MEROSS_EMAIL')
PASSWORD = os.environ.get('MEROSS_PASSWORD')
class TestMSS425ETest(unittest.TestCase):
def setUp(self):
self.counter = AtomicCounter(0)
set_log_level(INFO, INFO)
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('mss425e')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device mss425e")
def print_result(self, error, res):
# TODO: assertions
print("Error: %s, Result: %s" % (error, res))
print("Counter=%d" % self.counter.inc())
# TODO: This fails. We need to investigate why.
"""
def test_async(self):
for i in range(0, 40):
op = bool(random.getrandbits(1))
channel = random.randrange(0, len(self.device.get_channels()))
if not op:
self.device.turn_off_channel(channel, callback=self.print_result)
else:
self.device.turn_on_channel(channel, callback=self.print_result)
while self.counter.get() < 40:
time.sleep(1)
def test_sync(self):
for i in range(0, 30):
print("Executing command %d" % i)
time.sleep(0.01)
channel = random.randrange(0, len(self.device.get_channels()))
self.device.turn_off_channel(channel)
time.sleep(0.01)
self.device.turn_on_channel(channel)
time.sleep(0.01)
print("Done command %d" % i)
"""
def tearDown(self):
self.manager.stop()

View File

@ -0,0 +1,54 @@
from meross_iot.manager import MerossManager
import os
import time
import unittest
import random
EMAIL = os.environ.get('MEROSS_EMAIL')
PASSWORD = os.environ.get('MEROSS_PASSWORD')
class TestMSL120Test(unittest.TestCase):
def setUp(self):
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('msl120')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device msl120")
def test_power_cycle(self):
time.sleep(2)
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status()['onoff'])
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status()['onoff'])
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status()['onoff'])
def test_get_info(self):
state = self.device.get_status()
assert state is not None
def test_set_light_color(self):
r = int(random.random() * 255)
g = int(random.random() * 255)
b = int(random.random() * 255)
self.device.set_light_color(channel=0, rgb=(r, g, b))
time.sleep(5)
bulb_state = self.device.get_light_color(channel=0)
# TODO: RGB state is somehow normalized on the server side. We need to investigate the logic behind that...
def tearDown(self):
self.device.turn_off()
self.manager.stop()

View File

@ -0,0 +1,22 @@
import os
import unittest
from meross_iot.api import MerossHttpClient
EMAIL = os.environ.get('MEROSS_EMAIL')
PASSWORD = os.environ.get('MEROSS_PASSWORD')
class TestHttpMethods(unittest.TestCase):
def setUp(self):
self.client = MerossHttpClient(email=EMAIL, password=PASSWORD)
def test_device_listing(self):
devices = self.client.list_devices()
assert devices is not None
assert len(devices) > 0
def test_supported_device_listing(self):
devices = self.client.list_devices()
assert devices is not None
assert len(devices) > 0

View File

@ -0,0 +1,212 @@
import os
import time
import unittest
from meross_iot.manager import MerossManager
EMAIL = os.environ.get('MEROSS_EMAIL')
PASSWORD = os.environ.get('MEROSS_PASSWORD')
class TestMSS210Test(unittest.TestCase):
def setUp(self):
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('mss210')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device ms210")
def test_power_cycle(self):
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
def test_get_info(self):
state = self.device.get_status()
assert state is not None
wifi_list = self.device.get_wifi_list()
assert wifi_list is not None
trace = self.device.get_trace()
assert trace is not None
debug = self.device.get_debug()
assert debug is not None
def tearDown(self):
self.manager.stop()
class TestMSS310Test(unittest.TestCase):
def setUp(self):
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('mss310')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device mss310")
def test_power_cycle(self):
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
def test_get_info(self):
consumption = self.device.get_power_consumption()
assert consumption is not None
wifi_list = self.device.get_wifi_list()
assert wifi_list is not None
trace = self.device.get_trace()
assert trace is not None
debug = self.device.get_debug()
assert debug is not None
abilities = self.device.get_abilities()
assert abilities is not None
electricity = self.device.get_electricity()
assert electricity is not None
def tearDown(self):
self.manager.stop()
class TestMSS425ETest(unittest.TestCase):
def setUp(self):
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('mss425e')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device mss425e")
def test_power_cycle(self):
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
def test_usb(self):
self.device.enable_usb()
time.sleep(2)
self.assertTrue(self.device.get_usb_status())
self.device.enable_usb()
time.sleep(2)
self.assertTrue(self.device.get_usb_status())
def test_channels(self):
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
# Test each channel one by one
for c in self.device.get_channels():
self.device.turn_on_channel(c)
time.sleep(2)
self.assertTrue(self.device.get_channel_status(c))
time.sleep(2)
self.device.turn_off_channel(c)
time.sleep(2)
self.assertFalse(self.device.get_channel_status(c))
def test_get_info(self):
state = self.device.get_status()
assert state is not None
wifi_list = self.device.get_wifi_list()
assert wifi_list is not None
trace = self.device.get_trace()
assert trace is not None
debug = self.device.get_debug()
assert debug is not None
def tearDown(self):
self.manager.stop()
class TestMSS530HTest(unittest.TestCase):
def setUp(self):
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
self.manager.start()
# Retrieves the list of supported devices
devices = self.manager.get_devices_by_type('mss530h')
if len(devices) > 0:
self.device = devices[0]
else:
raise Exception("Could not find device mss530h")
def test_power_cycle(self):
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
self.device.turn_on()
time.sleep(2)
self.assertTrue(self.device.get_status())
self.device.turn_off()
time.sleep(2)
self.assertFalse(self.device.get_status())
def test_get_info(self):
state = self.device.get_status()
assert state is not None
wifi_list = self.device.get_wifi_list()
assert wifi_list is not None
trace = self.device.get_trace()
assert trace is not None
debug = self.device.get_debug()
assert debug is not None
def tearDown(self):
self.manager.stop()

@ -1 +0,0 @@
Subproject commit 85c67593dba9bb2df42681ba042e30fb0f3c719c

105
RPI Code/pyparrot_/pyparrot/.gitignore vendored Normal file
View File

@ -0,0 +1,105 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 amymcgovern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,2 @@
include pyparrot/commandsandsensors/*.xml
include pyparrot/utils/*.sdp

View File

@ -0,0 +1,69 @@
# pyparrot
Python interface for Parrot Drones
pyparrot was designed and implemented by Dr. Amy McGovern to program Parrot Mambo and Parrot Bebop 2
drones using python. This interface was developed to teach K-20 STEM concepts
(programming, math, and more) by programming a drone to fly autonomously.
Anyone can use it who is interested in autonomous drone programming!
# Installation, Quick-start, Documentation, FAQs
Extensive documentation is available at [https://pyparrot.readthedocs.io](https://pyparrot.readthedocs.io)
# Major updates and releases:
* 03/02/2019: Version 1.5.14: Fixed ffmpeg vision bug where it wasn't properly killing the ffmpeg subprocess
* 03/02/2019: Version 1.5.13: Added removal of old files in images directory by default to ffmpeg vision (can turn it off with a parameter)
* 02/19/2019: Version 1.5.12: Added pull request of wificonnection parameters and added ability to specify IP address (default uses mDNS still)
* 01/25/2019: Version 1.5.11: Added an example of using a cv2.namedWindow to show two vision windows (but it has issues on mac os 10.14 because it isn't a main thread)
* 10/29/2018: Version 1.5.10: Updated the groundcam to not break on disconnect with BLE. Also have updated documentation with slides from workshop and windows FAQs.
* 10/21/2018: Version 1.5.9: Fixed the wifiConnection without mDNS to work for Bebop (mDNS still works on bebop!). Verified that mambo and bebop work now with latest firmware.
* 10/19/2018: Version 1.5.8: Parrot broke mDNS in firmware 3.0.26 (and ftp is still broken). Disabled the groundcam and hard-coded the IP address and ports for the mambo. Long term we want mDNS back. tested backwards compatiblity on older firmware and it works.
* 10/13/2018: Version 1.5.7: Parrot released a security update/firmware upgrade to 3.0.25 that breaks ftp login for Mambo. pyparrot now allows the mambo to still connect without errors but the groundcam will not work until we hear from parrot. Also added example for joystick for the parrot swing from victor804
* 10/05/2018: Version 1.5.6: Removed a bug in the library on pypi where an old file was hanging around
* 09/19/2018: Version 1.5.5: Added joystink demo for swing (thanks Victor804)
* 09/06/2018: Version 1.5.4: Removed wait in indoor mode for bebop 1
* 09/06/2018: Version 1.5.3: Added indoor mode for bebop 1
* 8/30/2018: Version 1.5.2: Updated camera pan_tilt for Bebop 1 (thanks Victor804)
* 8/21/2018: Version 1.5.1: fixed small fix for typo in minidrones (for swing)
* 8/18/2018: Version 1.5.0: major update to suppport parrot swing drones (thank you Victor804). This does break a small backwards compatibility in that you need to import Mambo from Minidrone instead of Mambo. Everything else remains the same.
* 8/9/2018: Version 1.4.31: hard-coded name for vision stream on windows
* 8/9/2018: Version 1.4.30: fixed vision bug in windows using VLC (tempfile issues) and also made fps a parameter for VLC vision
* 7/16/2018: Version 1.4.29: added bebop user sensor callback function to match mambo
* 7/15/2018: Version 1.4.28: added bebop battery state to default state variables (was in the dictionary only before)
* 7/13/2018: Version 1.4.27: updated Mambo() initialization to not require address for wifi mode and also updated groundcam demo for Mambo
* 7/12/2018: Version 1.4.26: added new Bebop commands (mostly setting max limits for the bebop)
* 7/11/2018: Version 1.4.25: fixed groundcam pictures for Mambo
* 7/8/2018: Version 1.4.24: switched tempfile to back to NamedTemporaryFile in DroneVisionGUI due to OS incompatibilities
* 7/8/2018: Version 1.4.23: switched tempfile to SpooledTemporaryFile in DroneVisionGUI to make it faster (uses memory instead of disk)
* 7/6/2018: Version 1.4.22: Added a wait in flat_trim for Bebop until it is received (optional)
* 7/5/2018: Version 1.4.21: Added max_tilt and max_altitude to the Bebop commands.
* 7/4/2018: Version 1.4.20: While move_relative is implemented, it seems to have a firmware bug so DO NOT USE.
* 7/4/2018: Version 1.4.19: Added move_relative command to the Bebop API. For now, only dx, dy, and dradians should be used as there seems to be a bug internal to the firmware on dz.
* 6/17/2018: Version 1.4.18 Added landed button status to the Drone Vision GUI for safety in user code
* 6/16/2018: Version 1.4.17 Added flat trim to mambo also
* 6/16/2018: Version 1.4.16 Added flat trim to bebop
* 6/15/2018: Version 1.4.15 Removed a stray print, updated documentation, cast turn_degrees arguments to an int in Mambo.
* 6/11/2018: Version 1.4.14 Added bebop sdp file to the release on pip
* 6/7/2018: Version 1.4.13 Fixed duration in PCMD to use milliseconds instead of integer seconds
* 6/7/2018: Version 1.4.12 Added an option to fly_direct to allow the command to be sent once
* 6/6/2018: Version 1.4.11 Fixed a stray import statment not fixed from the move to pip
* 5/31/2018: Version 1.4.10 Documentation updated significantly and moved to readthedocs
* 5/30/2018: Version 1.4.7 and 1.4.8 and 1.4.9 fixed scripts location to release find_mambo script and added readthedocs documents
* 5/29/2018: Version 1.4.6 Accepted fixes for Bebop 1 compatibility
* 5/28/2018: Version 1.4.5 Fixed imports for new pypi structure and added xml files to pypi.
* 5/25/2018: Version 1.4.3. Uploaded to pypi so pyparrot can now be installed directory from pip. Updated documentation for new vision.
* 5/23/2018: Updated function (contributed) to download pictures from Mambo's downward facing camera.
* 3/25/2018: Added DroneVisionGUI which is a version of the vision that shows the video stream (for Bebop or Mambo) in real time.
* 2/22/2018: Version 1.3.2. Updated DroneVision to make the vision processing faster. Interface changed to only have the user call open_vision and close_vision (and not start_video_buffering)
* 2/10/2018: Version 1.3.1. Updated DroneVision to work on Windows.
* 2/8/2018: Version 1.3. Vision is working for both the Mambo and Bebop in a general interface called DroneVision. Major documenation updates as well.
* 2/6/2018: Updated Mambo to add speed settings for tilt & vertical. Needed for class.
* 2/4/2018: Unofficial updates to add ffmpeg support to the vision (will make an official release with examples soon)
* 12/09/2017: Version 1.2. Mambo now gives estimated orientation using quaternions. Bebop now streams vision, which is accessible via VLC or other video clients. Coming soon: opencv hooks into the vision.
* 12/02/2017: Version 1.1. Fixed sensors with multiple values for Mambo and Bebop.
* 11/26/2017: Initial release, version 1.0. Working wifi and BLE for Mambo, initial flight for Bebop.
# Programming and using your drones responsibly
It is your job to program and use your drones responsibly! We are not responsible for any losses or damages of your drones or injuries. Please fly safely and obey all laws.

View File

@ -0,0 +1,322 @@
"""
GUI for AI class using drones. Allows you to quickly create a map of
a room with obstacles for navigation and search.
Amy McGovern dramymcgovern@gmail.com
"""
from tkinter import *
import numpy as np
from tkinter import filedialog
import os
import pickle
class DroneGUI:
def __init__(self):
self.root = Tk()
self.room_map = None
self.obstacle_ids = None
self.factor = None
self.obstacle_color = "#7575a3"
self.goal_color = "green"
self.start_color = "red"
self.start_id = 2
self.goal_id = 3
self.obstacle_id = 1
def translate_click(self, event):
"""
Translate the click event into room coordinates and map coordinates
:param event:
:return:
"""
print("clicked at", event.x, event.y)
center_x = event.x
center_y = event.y
# calculate the lower left corner of the box
factor = 10 * self.scale_val
lower_x = int(center_x / factor) * factor
lower_y = int(center_y / factor) * factor
#print("lower x and y are ", lower_x, lower_y)
# translate the click into the map
map_x = int(center_x / self.factor)
map_y = self.room_map.shape[1] - int(center_y / self.factor) - 1
#print("map x and y are ", map_x, map_y)
return (center_x, center_y, lower_x, lower_y, map_x, map_y)
def toggle_obstacle_click(self, event):
"""
Toggle an obstacle with left button clicks
:param event: the tkinter event
:return: nothing
"""
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
if (self.room_map[map_x, map_y] == 0):
self.draw_obstacle(lower_x,lower_y, size=self.factor, color=self.obstacle_color, map_x=map_x, map_y=map_y)
self.room_map[map_x, map_y] = self.obstacle_id
else:
#print("Deleting obstacle ", self.obstacle_ids[map_x, map_y])
self.clear_obstacle(map_x, map_y)
self.room_map[map_x, map_y] = 0
def change_obstacle_type_click(self, event):
"""
Right click brings up a menu to let you choose what kind of obstacle this is
:param event:
:return:
"""
print("In popup menu")
popup_menu = Menu(self.root, tearoff=0)
popup_menu.add_command(label="Set to start", command=lambda: self.draw_start_click(event))
popup_menu.add_command(label="Set to goal", command=lambda: self.draw_goal_click(event))
popup_menu.add_command(label="Remove", command=lambda: self.toggle_obstacle_click(event))
popup_menu.post(event.x, event.y)
def draw_goal_click(self, event):
"""
Draw a green box for a goal at button 1 clicks
:param event:
:return:
"""
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
# clear whatever was there
self.clear_obstacle(map_x, map_y)
self.room_map[map_x, map_y] = 0
# and save the click into the map
self.room_map[map_x, map_y] = self.goal_id
self.draw_obstacle(lower_x,lower_y, self.factor, color=self.goal_color, map_x=map_x, map_y=map_y)
def draw_start_click(self, event):
"""
Draw a red box for the start
:param event:
:return:
"""
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
# clear whatever was there
self.clear_obstacle(map_x, map_y)
self.room_map[map_x, map_y] = 0
# and save the click into the map
self.room_map[map_x, map_y] = self.start_id
self.draw_obstacle(lower_x,lower_y, self.factor, color=self.start_color, map_x=map_x, map_y=map_y)
def draw_obstacle(self, x, y, size, color, map_x, map_y):
# draw the rectangle
obs_id = self.room_canvas.create_rectangle(x, y, x + size, y + size, fill=color)
#print("Obstacle id is ", obs_id)
self.obstacle_ids[map_x, map_y] = obs_id
def clear_obstacle(self, map_x, map_y):
# draw the rectangle
self.room_canvas.delete(self.obstacle_ids[map_x, map_y])
self.obstacle_ids[map_x, map_y] = 0
def set_scale(self):
try:
self.scale_val = int(self.scale.get())
except:
self.scale_val = 1
# set the factor used for drawing translations
self.factor = 10 * self.scale_val
def create_room(self):
"""
Create the window with the room grid
Uses the scale parameter set from the first gui window to decide how big the boxes are (scale must be an int)
Draws a grid with black lines every 10 * scale pixels (e.g. every decimeter) and then draws a thicker
line every meter (e.g. every 10 lines)
:return:
"""
length = float(self.length.get())
height = float(self.height.get())
# initialize the internal map
self.room_map = np.zeros((int(length * 10), int(height * 10)))
self.obstacle_ids = np.zeros((int(length * 10), int(height * 10)), dtype='int')
print(self.room_map.shape)
print("Length is %0.1f and height is %0.1f" % (length, height))
self.set_scale()
self.draw_room(length, height)
def draw_room(self, length, height):
# each pixel is scale * 1 cm so multiply by 100 to get the width/height from the meters
canvas_width = int(length * 100 * self.scale_val)
canvas_height = int(height * 100 * self.scale_val)
# create the blank canvas
room = Toplevel(self.root)
# put the menu into the room
# menu code mostly from
# https://www.python-course.eu/tkinter_menus.php
menu = Menu(room)
room.config(menu=menu)
filemenu = Menu(menu)
menu.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="Save Map", command=self.save_file_menu)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=self.root.quit)
helpmenu = Menu(menu)
menu.add_cascade(label="Help", menu=helpmenu)
helpmenu.add_command(label="About...", command=self.about_menu)
# draw the room
self.room_canvas = Canvas(room, width=canvas_width, height=canvas_height, bg="#ffffe6")
self.room_canvas.pack()
# how to draw a checkered canvas from
# https://www.python-course.eu/tkinter_canvas.php
# vertical lines at an interval of "line_distance" pixel
line_distance = 10 * self.scale_val
for x in range(line_distance, canvas_width, line_distance):
if (x % (line_distance * 10) == 0):
self.room_canvas.create_line(x, 0, x, canvas_height, fill="red", width=2)
else:
self.room_canvas.create_line(x, 0, x, canvas_height, fill="black")
# horizontal lines at an interval of "line_distance" pixel
for y in range(line_distance, canvas_height, line_distance):
if (y % (line_distance * 10) == 0):
self.room_canvas.create_line(0, y, canvas_width, y, fill="red", width=2)
else:
self.room_canvas.create_line(0, y, canvas_width, y, fill="black")
# bind the button clicks to draw out the map
self.room_canvas.bind("<Button-1>", self.toggle_obstacle_click)
self.room_canvas.bind("<Button-2>", self.change_obstacle_type_click)
# add in the obstacles (if any exist already)
(xs, ys) = np.nonzero(self.room_map)
factor = 10 * self.scale_val
for i, x in enumerate(xs):
y = ys[i]
lower_x = x * factor
lower_y = (self.room_map.shape[1] - y - 1) * factor
if (self.room_map[x, y] == 1):
self.draw_obstacle(lower_x, lower_y, factor, color="#7575a3", map_x=x, map_y=y)
elif (self.room_map[x, y] == 2):
self.draw_obstacle(lower_x, lower_y, factor, color="red", map_x=x, map_y=y)
elif (self.room_map[x, y] == 3):
self.draw_obstacle(lower_x, lower_y, factor, color="green", map_x=x, map_y=y)
def draw_map_from_file(self):
width = self.room_map.shape[1] / 10.0
length = self.room_map.shape[0] /10.0
#print("length and width of loaded room are ", length,width)
#print("Scale is ", self.scale_val)
self.draw_room(length, width)
def open_file_menu(self):
"""
Load a map from a file
:return:
"""
filename = filedialog.askopenfilename(initialdir=os.getcwd(),
title="Select map file",
filetypes=(("map files", "*.map"), ("all files", "*.*")))
fp = open(filename, "rb")
self.scale_val = pickle.load(fp)
self.room_map = pickle.load(fp)
#print("scale val is ", self.scale_val)
#print("room map is ", self.room_map)
fp.close()
self.draw_map_from_file()
def save_file_menu(self):
"""
Bring up a save file dialog and then save
:return:
"""
filename = filedialog.asksaveasfile(initialdir=os.getcwd(),
title="Save map file",
defaultextension=".map")
print("saving to ", filename.name)
fp = open(filename.name, "wb")
pickle.dump(self.scale_val, fp)
pickle.dump(self.room_map, fp)
fp.close()
def about_menu(self):
pass
def draw_initial_gui(self):
"""
Draws the intial GUI that lets you make a new room
or load one from a file
:return:
"""
# menu code mostly from
# https://www.python-course.eu/tkinter_menus.php
menu = Menu(self.root)
self.root.config(menu=menu)
filemenu = Menu(menu)
menu.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="Open Map", command=self.open_file_menu)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=self.root.quit)
helpmenu = Menu(menu)
menu.add_cascade(label="Help", menu=helpmenu)
helpmenu.add_command(label="About...", command=self.about_menu)
# draw the request to create a new room
Label(self.root, text="Enter the size of the room you are flying in (decimals to tenths)").grid(row=0, columnspan=2)
Label(self.root, text="Length (x) (meters)").grid(row=1)
Label(self.root, text="Height (y) (meters)").grid(row=2)
Label(self.root, text="1 cm = _ pixels").grid(row=3)
# the entry boxes
self.length = Entry(self.root)
self.length.grid(row=1, column=1)
self.height = Entry(self.root)
self.height.grid(row=2, column=1)
self.scale = Entry(self.root)
self.scale.grid(row=3, column=1)
# action buttons
Button(self.root, text='Quit', command=self.root.quit).grid(row=4, column=0, pady=4)
Button(self.root, text='Create room', command=self.create_room).grid(row=4, column=1, pady=4)
def go(self):
"""
Start the main GUI loop
:return:
"""
mainloop()
if __name__ == "__main__":
gui = DroneGUI()
gui.draw_initial_gui()
gui.go()

View File

@ -0,0 +1,59 @@
"""
Demo the direct flying for the python interface
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
# you will need to change this to the address of YOUR mambo
mamboAddr = "d0:3a:69:b9:e6:5a"
# make my mambo object
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
mambo = Mambo(mamboAddr, use_wifi=False)
print("trying to connect")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
if (success):
# get the state information
print("sleeping")
mambo.smart_sleep(2)
mambo.ask_for_state_update()
mambo.smart_sleep(2)
print("taking off!")
mambo.safe_takeoff(5)
"""
print("Flying direct: going forward (positive pitch)")
mambo.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
print("Showing turning (in place) using turn_degrees")
mambo.turn_degrees(90)
mambo.smart_sleep(2)
mambo.turn_degrees(-90)
mambo.smart_sleep(2)
print("Flying direct: yaw")
mambo.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
print("Flying direct: going backwards (negative pitch)")
mambo.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
print("Flying direct: roll")
mambo.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
print("Flying direct: going up")
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
mambo.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=3)
"""
print("landing")
mambo.safe_land(5)
mambo.smart_sleep(5)
print("disconnect")
mambo.disconnect()

View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = pyparrot
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,28 @@
.. title:: About pyparrot
.. about:
About the pyparrot project
==========================
About pyparrot
-----------------
Pyparrot was developed by `Dr. Amy McGovern <http://www.mcgovern-fagg.org/amy/>`_ with the support of
the `University of Oklahoma <https://www.kipr.org>`_ and
the `Kiss Institute for Practical Robotics <https://www.kipr.org>`_. The original goal was to teach children to program
using educational programs like `botball <http://www.botball.org>`_. The pyparrot project has been adopted by groups
around the world and has been used in both K-12 settings and at the university level.
Educational Programs Using pyparrot
-----------------------------------
If you would like to be added to this list, please email dramymcgovern @ gmail.com (without the spaces).
* `Botball <http://www.botball.org>`_
* `University of Oklahoma School of Computer Science <http://www.ou.edu/coe/cs>`_
* `Talenteahaus <http://www.talentehaus.at>`_
* `Tech Garage <https://tech-garage.org>`_
* `St Eugene College <http://www.steugene.qld.edu.au>`_
* `KIPR/botball <http://www.kipr.org/>`_

View File

@ -0,0 +1,164 @@
.. title:: Bebop Commands and Sensors
.. bebopcommands:
Bebop Commands and Sensors
==============================
Bebop commands
--------------
Each of the public commands available to control the bebop is listed below with its documentation.
The code is also well documented and you can also look at the API through readthedocs.
All of the functions preceeded with an underscore are intended to be internal functions and are not listed below.
Creating a Bebop object
^^^^^^^^^^^^^^^^^^^^^^^
``Bebop(drone_type="Bebop2")`` create a Bebop object with an optional drone_type argument that can be used to create
a bebop one or bebop 2 object. Default is Bebop 2. Note, there is limited support for the original bebop since
I do not own one for testing.
Connecting and disconnecting
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``connect(num_retries)`` connect to the Bebop's wifi services. This performs a handshake.
This can take several seconds to ensure the connection is working.
You can specify a maximum number of re-tries. Returns true if the connection suceeded or False otherwise.
``disconnect()`` disconnect from the wifi connection
Takeoff and landing
^^^^^^^^^^^^^^^^^^^
``takeoff()`` Sends a single takeoff command to the bebop. This is not the recommended method.
``safe_takeoff(timeout)`` This is the recommended method for takeoff. It sends a command and then checks the
sensors (via flying state) to ensure the bebop is actually taking off. Then it waits until the bebop is
flying or hovering to return. It will timeout and return if the time exceeds timeout seconds.
``land()`` Sends a single land command to the bebop. This is not the recommended method.
``safe_land(timeout)`` This is the recommended method to land the bebop. Sends commands
until the bebop has actually reached the landed state. It will timeout and return if the time exceeds timeout seconds.
Flying
^^^^^^
``flip(direction)`` Sends the flip command to the bebop. Valid directions to flip are: front, back, right, left.
``fly_direct(roll, pitch, yaw, vertical_movement, duration)`` Fly the bebop directly using the
specified roll, pitch, yaw, and vertical movements. The commands are repeated for duration seconds.
Note there are currently no sensors reported back to the user to ensure that these are working but hopefully
that is addressed in a future firmware upgrade. Each value ranges from -100 to 100 and is essentially a percentage
and direction of the max_tilt (for roll/pitch) or max_vertical_speed (for vertical movement).
``move_relative(dx, dy, dz, dradians)`` Moves the bebop a relative number of meters in x (forward/backward,
forward is positive), y (right/left, right is positive), dz (up/down, positive is down), and dradians.
If you use this command INDOORS, make sure you either have FULL GPS coverage or NO GPS coverage (e.g. cover the front of the bebop
with tin foil to keep it from getting a lock). If it has mixed coverage, it randomly flies at high speed in random
directions after the command executes. This is a known issue in Parrot's firmware and they state that a fix is coming.
``set_max_altitude(altitude)`` Set the maximum allowable altitude in meters.
The altitude must be between 0.5 and 150 meters.
``set_max_distance(distance)`` Set max distance between the takeoff and the drone in meters.
The distance must be between 10 and 2000 meters.
``enable_geofence(value)`` If geofence is enabled, the drone won't fly over the given max distance.
Valid value: 1 if the drone can't fly further than max distance, 0 if no limitation on the drone should be done.
``set_max_tilt(tilt)`` Set the maximum allowable tilt in degrees for the drone (this limits speed).
The tilt must be between 5 (very slow) and 30 (very fast) degrees.
``set_max_tilt_rotation_speed(speed)`` Set the maximum allowable tilt rotation speed in degree/s.
The tilt rotation speed must be between 80 and 300 degree/s.
``set_max_vertical_speed(speed)`` Set the maximum allowable vertical speed in m/s.
The vertical speed must be between 0.5 and 2.5 m/s.
``set_max_rotation_speed(speed)`` Set the maximum allowable rotation speed in degree/s.
The rotation speed must be between 10 and 200 degree/s.
``set_flat_trim(duration=0)`` Tell the Bebop to run with a flat trim. If duration > 0, waits for the comand to be acknowledged
``set_hull_protection(present)`` Set the presence of hull protection (only for bebop 1).
The value must be 1 if hull protection is present or 0 if not present. This is only useful for the bebop 1.
``set_indoor(is_outdoor)`` Set the bebop 1 (ignored on bebop 2) to indoor or outdoor mode.
The value must be 1 if bebop 1 is outdoors or 0 if it is indoors. This is only useful for the bebop 1.
Pausing or sleeping in a thread safe manner
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``smart_sleep(seconds)`` This sleeps the number of seconds (which can be a floating point) but wakes for all
wifi notifications. You should use this instead of time.sleep to be consistent with the mambo but it is not
required (whereas time.sleep() will break a mambo using BLE).
Video camera
^^^^^^^^^^^^
``start_video_stream()``: tells the bebop to start streaming the video. These are really intended to be
called within the DroneVision or DroneVisionGUI functions and not directly by the user (but you can call
them directly if you are writing your own vision routines).
``stop_video_stream()``: tells the bebop to stop streaming the video. Same as above: intended to be called
by the DroneVision or DroneVisionGUI routines.
``set_video_stream_mode(mode)``: set the video mode to one of three choices: "low_latency",
"high_reliability", "high_reliability_low_framerate". low_latency is the default.
``pan_tilt_camera(tilt_degrees, pan_degrees)``: Send the command to pan/tilt the camera by the specified number of degrees in pan/tilt.
Note, this only seems to work in small increments. Use pan_tilt_velocity to get the camera to look straight downward.
``pan_tilt_camera_velocity(self, tilt_velocity, pan_velocity, duration=0)``: Send the command to tilt the camera by
the specified number of degrees per second in pan/tilt. This function has two modes. First, if duration is 0,
the initial velocity is sent and then the function returns (meaning the camera will keep moving).
If duration is greater than 0, the command executes for that amount of time and then sends a stop command to
the camera and then returns.
``set_picture_format(format)``: Change the picture format to raw, jpeg, snapshot or jpeg_fisheye.
``set_white_balance(type)``: Change the type of white balance between: auto, tungsten, daylight, cloudy or cool_white.
``set_exposition(value)``: Change the image exposition between -1.5 and 1.5.
``set_saturation(value)``: Change the image saturation between -100 and 100.
``set_timelapse(enable, interval)``: To start a timelapse set enable at 1 and an interval between 8 and 300 sec.
To stop the timelapse just set enable to 0.
``set_video_stabilization(mode)``: Change the video stabilization between 4 modes: roll_pitch, pitch, roll, none.
``set_video_recording(mode)``: Change the video recording mode between quality and time.
``set_video_framerate(framerate)``: Change the video framerate between: 24_FPS, 25_FPS or 30_FPS.
``set_video_resolutions(type)``: Change the video resolutions for stream and rec between rec1080_stream480, rec720_stream720.
Sensor commands
^^^^^^^^^^^^^^^
``ask_for_state_update()`` This sends a request to the bebop to send back ALL states. The data returns
fairly quickly although not instantly. The bebop already has a sensor refresh rate of 10Hz but not all sensors are sent
automatically. If you are looking for a specific sensor that is not automatically sent, you can call this but I don't
recommend sending it over and over. Most of the sensors you need should be sent at either the 10Hz rate or as an event
is called that triggers that sensor.
Bebop sensors
-------------
All of the sensor data that is passed back to the Bebop is saved in a python dictionary. As needed, other variables
are stored outside the dictionary but you can get everything you need from the dictionary itself. All of the data
is stored in the BebopSensors class.
The easiest way to interact with the sensors is to call:
``bebop.set_user_sensor_callback(function, args)``. This sets a user callback function with optional
arguments that is called each time a sensor is updated. The refresh rate on wifi is 10Hz.
The sensors are:
* battery (defaults to 100 and stays at that level until a real reading is received from the drone)
* flying_state: This is updated as frequently as the drone sends it out and can be one of "landed", "takingoff", "hovering", "flying", "landing", "emergency", "usertakeoff", "motor_ramping", "emergency_landing". These are the values as specified in `ardrone3.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/ardrone3.xml>`_.
* sensors_dict: all other sensors are saved by name in a dictionary. The names come from the `ardrone3.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/ardrone3.xml>`_ and `common.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/common.xml>`_.

View File

@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# pyparrot documentation build configuration file, created by
# sphinx-quickstart on Tue May 29 13:55:14 2018.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
# the mock stuff was borrowed from hagelslag to help make things work on readthedocs
from mock import Mock as MagicMock
class Mock(MagicMock):
@classmethod
def __getattr__(cls, name):
return Mock()
MOCK_MODULES = ['numpy', 'scipy', 'zeroconf', 'cv2', 'untangle', 'bluepy', 'bluepy.btle',
'ipaddress', 'queue', 'http.server', 'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui',
'PyQt5.QtWidgets']
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.mathjax',
'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md']
#source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'pyparrot'
copyright = '2018, Amy McGovern'
author = 'Amy McGovern'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.5'
# The full version, including alpha/beta/rc tags.
release = '1.5.3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyparrotdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pyparrot.tex', 'pyparrot Documentation',
'Amy McGovern', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pyparrot', 'pyparrot Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pyparrot', 'pyparrot Documentation',
author, 'pyparrot', 'One line description of project.',
'Miscellaneous'),
]

View File

@ -0,0 +1,20 @@
.. title:: Contact the pyparrot developers
.. contact:
Contact the pyparrot developers
===============================
Contribute
----------
We welcome your contributions via bug report or pull request.
* Issue Tracker: `<https://github.com/amymcgovern/pyparrot/issues>`_
* Pull requests: `<https://github.com/amymcgovern/pyparrot/pulls>`_
* Source Code: `<https://github.com/amymcgovern/pyparrot>`_
Support
-------
If you are having issues, please let us know by reporting issues on GitHub using the issue
tracker `<https://github.com/amymcgovern/pyparrot/issues>`_.

View File

@ -0,0 +1,62 @@
.. title:: Frequently Asked Questions
.. faq:
Frequently Asked Questions
====================================
Below is a list of common errors and how to fix them.
Vision isn't showing anything on the minidrone
----------------------------------------------
The Minidrone camera puts itself into a "resting" state after not flying for several minutes. To solve this, you
either need to fly again (a simple takeoff and landing will suffice) or reboot the minidrone and reconnect.
I'm using windows and my drone gives me lots of timeout errors
---------------------------------------------------------------
This is a windows security setting and it can be fixed. Go into your windows firewall settings (control panel,
system and security, allow a program through Windows Firewall) and change the settings
for python.exe to be allowed through the firewall for both home/private networks and public networks. Your sensors will
suddenly be able to send data to your machine and safe_land will start working again as well as any sensors!
My drone does takeoff and landing but nothing else
--------------------------------------------------
Likely you have the remote controller on and attached! For some reason, if the remote is on,
it will allow the python code to takeoff & land but no other commands will work.
Turn off the remote and your code should work fine!
Errors connecting to the drone
------------------------------
There are two common errors that I see when flying. One requires the drone to reboot and one requires the
computer controlling the drone to reboot.
Connection failed
^^^^^^^^^^^^^^^^^
If you fail to connect to the drone, you will see an error message like this:
::
connection failed: did you remember to connect your machine to the Drone's wifi network?
The most likely cause is that you forgot to connect to the drone's wifi. If you tried to connect,
sometimes that connection fails. Try again or let the connection sit for a minute and try your program again.
If you are on the wifi but you get connection refused errors, reboot the drone.
Address in use
^^^^^^^^^^^^^^
The second common error is about the address being in use, as shown below.
::
OSError: [Errno 48] Address already in use
There are two ways to fix this, depending on the issue. It is possible you tried to run a second program while
you still had a first program running. If this is the case, make sure you stop all of your minidrone programs and then
restart only one. If you are not running a second minidrone program, then the solution is to reboot. This sometimes
happens due to the program crashing before it releases the socket.

View File

@ -0,0 +1,13 @@
.. title:: Slides from Workshops Teaching PyParrot
.. gettingstartedslides:
Workshop Materials and Slides
==============================
We have taught several workshops on using pyparrot. As we continue to teach and develop materials for
pyparrot, we will continue to share the curriculum materials here. Since the 2018 OK workshop slides supercede the
GCER 2018 slides, we only share the 2018 OK Workshop slides for now.
* Morning slides from the OK workshop (focus on initial setup, initial flying) :download:`OK2018Morning.pdf <OK2018Morning.pdf>`
* Afternoon slides from the OK workshop (learning to use sensors, extra slides on vision though we did not discuss at workshop) :download:`OK2018Afternoon.pdf <OK2018Afternoon.pdf>`.

View File

@ -0,0 +1,38 @@
.. pyparrot documentation master file, created by
sphinx-quickstart on Tue May 29 13:16:36 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyparrot's documentation!
====================================
pyparrot was designed by Dr. Amy McGovern to program Parrot Minidrone (primarily Mambo FPV but Swing is also
supported) and Parrot Bebop (1 or 2) drones using Python. This interface was developed to teach kids of all ages (K-20) STEM
concepts (programming, math, and more) by having them program a drone
to fly autonomously. Anyone can use it who is interested in
autonomous drone programming!
Main documentation
==========================
.. toctree::
:maxdepth: 3
installation.rst
quickstartminidrone.rst
quickstartbebop.rst
gettingstartedslides.rst
minidronecommands.rst
bebopcommands.rst
vision.rst
faq.rst
contact.rst
about.rst
license.rst
modules.rst
Indices and tables
==================
* :ref:`modindex` is a good place to start if you want to read API docs
* :ref:`genindex` of ALL functions (warning, this is huge and overwhelming)
* :ref:`search`

View File

@ -0,0 +1,189 @@
.. title:: Installation
.. installation:
Installation
===============
You have two choices for installing pyparrot: using the ``source`` code directly or downloading with ``pip``.
**Note** Pyparrot will only work with python 3. This choice was made because the support for multi-threaded
programs is improved in python 3.
Requirements
------------
The choice of related packages is dependent on your choice of drone (Mambo, Mambo FPV, Bebop 1 or 2, Swing, Anafi) and
to the operating system that you will be using to develop.
Hardware/Drone requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^^
* **Parrot Mambo FPV**: If you have a Mambo FPV (e.g. you have the camera), you can use the wifi interface. The wifi interface will work on Mac, Linux, or Windows.
* **Parrot Mambo Fly or Code**: If you have a Mambo without the camera, you will use the BLE interface. pyparrot currently only supports Linux for BLE. The BLE interface was developed on a Raspberry Pi 3 Model B but it has been tested on other Linux machines.
* **Parrot Swing**: To use the Swing you will use the BLE interface.
* **Parrot Bebop 2**: The Bebop interface was tested on a Bebop 2 using a laptop with wifi (any wifi enabled device should work).
* **Parrot Bebop 1**: A Bebop 1 will also work with any wifi enabled device.
* **Parrot Anafi**: Per the development board, the Anafi should work with very minor changes. I will work to officially suppport it once the SDK from parrot is released for it.
Software requirements
^^^^^^^^^^^^^^^^^^^^^
Software requirements are listed below by type of connection to the drone.
* All drones: Python 3
I use the `<https://www.anaconda.com/download/>`_:: installer and package manager for python. Note, when you
install anaconda, install the Visual Studio option, especially if you have windows. Otherwise you will need to install
Visual Studio separately. The zeroconf package (listed below) requires developer tools because it needs to be compiled.
* All drones: untangle package (this is used to parse the xml files in the parrot SDK)
::
pip install untangle
* Vision: If you intend to process the camera files, you will need to install opencv and then either ffmpeg
or VLC. I installed ffmpeg using brew for the mac but apt-get on linux should also work. For VLC, you MUST install
the actual `VLC <https://www.videolan.org/vlc/index.html`_ program (and not just the library in python)
and it needs to be version 3.0.1 or greater.
* Wifi connection: `zeroconf <https://pypi.python.org/pypi/zeroconf>`_ To install zeroconf software do the following:
::
pip install zeroconf
* BLE connection: pybluez (note this is ONLY for support without the camera!) This is ONLY supported on linux.
To install the BLE software do the following:
::
sudo apt-get install bluetooth
sudo apt-get install bluez
sudo apt-get install python-bluez
Note it is also possible that you will need to install bluepy (if it isn't already there). These commands should do it:
::
sudo apt-get install python-pip libglib2.0-dev
sudo pip install bluepy
sudo apt-get update
Installing From Source
----------------------
First download pyparrot by cloning the repository from `<https://github.com/amymcgovern/pyparrot>`_ The instructions for this are below.
::
git clone https://github.com/amymcgovern/pyparrot
cd pyparrot
Make sure you install the necessary other packages (wifi or BLE, vision, etc) as specified above.
Installing From Pip
-------------------
To install from pip, type
::
pip install pyparrot
Make sure you install the necessary other packages (wifi or BLE, vision, etc) as specified above.
Installation guide for windows users who might need more help
-------------------------------------------------------------
Thank you to @JackdQuinn for contributing this.
Make sure you install **Visual Studio** either using Anaconda or by downloading it from Microsoft. Note that Visual
Studio is free but it is required for compilation of the wifi module zeroconf, and specifically of the netifaces
module that zeroconf requires. It is a very large download if you chose to do it outside of anaconda so you will
want to start that download first.
If you install python without anaconda, when you install choose Special install Python and
click add python to path (this will clear up some command line call issues).
Again, if you chose regular python and not anaconda, you can check installation by typing py in the windows command line.
::
py
Once you are sure that python started, you will want to quit python. type: ``quit()`` to exit python
::
quit()
If you chose to use anaconda, bring up the anaconda menu and open an anaconda prompt to verify that it installed.
The rest of the instructions depend on whether you chose python or anaconda for your installation. If you chose python,
use the windows command prompt for pip. If you chose anaconda, use your anaconda prompt.
If you type the pip command (with no options), it will produce a long list of options. This tells you that you
are at the right command prompt to do the rest of the installation.
**Note, the pip command will not work inside of python.** This is a command prompt command, not a python command.
::
pip
Sometimes pip tells you that it wants to upgrade. For windows, the command is:
::
python -m pip install -U pip
To actually install, use the commands described above (and repeated here).
::
pip install untangle
pip install pyparrot
pip install zeroconf
**Note that visual studio is a requirement for zeroconf**
Testing your install
^^^^^^^^^^^^^^^^^^^^
The first step is to connect your connect your controlling device (laptop, computer, etc) to the wifi for the drone.
Look for a wifi network named Mambo_number where number changes for each drone.
After connection to your drone its time to run code! You can download all the example code from
these docs. Below is a short set of commands of how to run that code.
Run code by cd'ing down to the directory (the folder your python code is in) and running the desired python file from the cmd line
Example:
* open command line either through windows or anaconda (depending on your installation method)
* type: ``cd desktop``
* this will Change your Directory to the desktop
* type: ``dir``
* this will display a list of all the folders (directories) on the desktop
* type: ``cd yourFolderNameHere``
* type: ``dir``
* this will display all the files and folders in the directory
* type: ``py TheNameOfTheFileYouWantToRun.py`` or ``python TheNameOfTheFileYouWantToRun.py``
* When you click enter the file will begin to run, if you are using the demo scripts you should see lots of nice feedback as it changes states. You can use the arrow keys to go through your history of commands which can save you lots of time if your file names are long.
* If you have several connects and disconnects try restarting your computer or resetting your ip (for the more technically inclined)
* If you have crashes where the drone is flipping to one side when it shouldn't check the blades and bumpers. The bumpers can shift after a crash and prevent the blades from spinning, or slow down their spin, which causes unintended flips

View File

@ -0,0 +1,32 @@
.. title:: License
.. license:
License
===============
MIT License
-----------
The project is licensed under the MIT License.
Copyright (c) 2017 amymcgovern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,152 @@
.. title:: Minidrone Commands and Sensors
.. minidronecommands:
Minidrone Commands and Sensors
==============================
Minidrone commands
--------------
Each of the public commands available to control the minidrone is listed below with its documentation.
The code is also well documented and you can also look at the API through readthedocs.
All of the functions preceeded with an underscore are intended to be internal functions and are not listed below.
Creating a Mambo object
^^^^^^^^^^^^^^^^^^^^^^^
``Mambo(address="", use_wifi=True/False)``
create a mambo object with the specific harware address (found using findMinidrone). The use_wifi argument defaults to
False (which means BLE is the default). Set to True to use wifi. You can only use wifi if you have a FPV camera
installed on your Mambo! If you are using wifi, the hardware address argument can be ignored (it defaults to an empty
string).
Creating a Swing object
^^^^^^^^^^^^^^^^^^^^^^^
``Swing(address="")``
create a Swing object with the specific harware address (found using findMinidrone).
Connecting and disconnecting
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``connect(num_retries)`` connect to the Minidrone either using BLE services and characteristics or wifi
(specified when you created the Mambo object). This can take several seconds to ensure the connection is working.
You can specify a maximum number of re-tries. Returns true if the connection suceeded or False otherwise.
``disconnect()`` disconnect from the BLE or wifi connection
Takeoff and landing
^^^^^^^^^^^^^^^^^^^
``safe_takeoff(timeout)`` This is the recommended method for takeoff. It sends a command and then checks the
sensors (via flying state) to ensure the minidrone is actually taking off. Then it waits until the minidrone is
flying or hovering to return. It will timeout and return if the time exceeds timeout seconds.
``safe_land(timeout)`` This is the recommended method to land the minidrone. Sends commands
until the minidrone has actually reached the landed state. It will timeout and return if the time exceeds timeout seconds.
``takeoff()`` Sends a single takeoff command to the minidrone. This is not the recommended method.
``land()`` Sends a single land command to the minidrone. This is not the recommended method.
``turn_on_auto_takeoff()`` This puts the minidrone in throw mode. When it is in throw mode, the eyes will blink.
Flying
^^^^^^
``hover()`` and ``set_flat_trim()`` both tell the drone to assume the current configuration is a flat trim and it will
use this as the default when not receiving commands. This enables good hovering when not sending commands.
``flip(direction)`` Sends the flip command to the minidrone. Valid directions to flip are: front, back, right, left.
``turn_degrees(degrees)`` Turns the minidrone in place the specified number of degrees.
The range is -180 to 180. This can be accomplished in direct_fly() as well but this one uses the
internal minidrone sensors (which are not sent out right now) so it is more accurate.
``fly_direct(roll, pitch, yaw, vertical_movement, duration)`` Fly the minidrone directly using the
specified roll, pitch, yaw, and vertical movements. The commands are repeated for duration seconds.
Note there are currently no sensors reported back to the user to ensure that these are working but hopefully
that is addressed in a future firmware upgrade. Each value ranges from -100 to 100 and is essentially a percentage
and direction of the max_tilt (for roll/pitch) or max_vertical_speed (for vertical movement).
``set_max_tilt(degrees)`` Set the maximum tilt in degrees. Be careful as this makes your drone go slower or faster!
It is important to note that the fly_direct command uses this value in conjunction with the -100 to 100 percentages.
``set_max_vertical_speed(speed)`` Set the maximum vertical speed in m/s. Be careful as this makes your drone go up/down faster!
Pausing or sleeping in a thread safe manner
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``smart_sleep(seconds)`` This sleeps the number of seconds (which can be a floating point) but wakes for all
BLE or wifi notifications. **Note, if you are using BLE: This comamnd is VERY important**. **NEVER** use regular
time.sleep() as your BLE will disconnect regularly! Use smart_sleep instead! time.sleep() is ok if you are using
wifi but smart_sleep() handles that for you.
USB accessories: Claw and Gun
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``open_claw()`` Open the claw. Note that the claw should be attached for this to work.
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the claw with the FPV camera attached.
``close_claw()`` Close the claw. Note that the claw should be attached for this to work.
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the claw with the FPV camera attached.
``fire_gun()`` Fires the gun. Note that the gun should be attached for this to work.
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the gun with the FPV camera attached.
Swing specific commands
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``set_plane_gear_box(state)`` Choose the swing angle in plane mode. There are 3 tilt modes: gear_1, gear_2, gear_3.
Warning gear_3 is very fast.
``set_flying_mode(mode)`` Choose flight mode between: quadricopter, plane_forward, plane_backward.
Ground facing camera
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``take_picture()``` The minidrone will take a picture with the downward facing camera. It only stores up to 40 pictures
internally so this function deletes them after 35 have been taken. Make sure you are downloading them either
using the mobile interface or through the python code.
**Note**: Parrot broke the ability to access the groundcam in their latest (3.0.25) firmware upgrade. We will reenable these
functions as soon as parrot fixes the firmware but for now, they will only work in versions 3.0.24 and below.
``get_groundcam_pictures_names()`` Returns the names of the pictures stored internally from the groundcam. Only for the mambo.
``get_groundcam_picture(name)`` Returns the picture with the specified name. Only for the mambo.
Sensor related commands
^^^^^^^^^^^^^^^^^^^^^^^
``ask_for_state_update()`` This sends a request to the minidrone to send back ALL states
(this includes the claw and gun states). This really only needs to be called once at the start of the program
to initialize some of the state variables. If you are on wifi, many of the other variables are sent at 2Hz. If you are
on BLE, you will want to use this command to get more state information but keep in mind it will be slow.
This command will return immediately but you should wait a few seconds before using the new state information
as it has to be updated.
Mambo sensors
-------------
All of the sensor data that is passed back to the program is saved. Note that Parrot sends back more
information via wifi than via BLE, due to the limited BLE bandwidth. The sensors are saved in Minidrone.sensors.
This is an instance of a MamboSensors class, which can be seen at the top of the Minidrone.py file.
The easiest way to interact with the sensors is to call:
``minidrone.set_user_sensor_callback(function, args)``. This sets a user callback function with optional
arguments that is called each time a sensor is updated. The refresh rate on wifi is 2Hz.
The sensors are:
* battery (defaults to 100 and stays at that level until a real reading is received from the drone)
* flying_state: This is updated as frequently as the drone sends it out and can be one of "landed", "takingoff", "hovering", "flying", "landing", "emergency", "rolling", "init". These are the values as specified in `minidrone.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/minidrone.xml>`_.
* gun_id: defaults to 0 (as far as I can tell, it is only ever 0 when it comes from the drone anyway)
* gun_state: "READY" or "BUSY" as sent by the drone, if a gun is attached. Defaults to None.
* claw_id: defaults to 0
* claw_state: "OPENING", "OPENED", "CLOSING", "CLOSED" as sent by the drone, if a claw is attached. Defaults to None.
* speed_x, speed_y, speed_z, speed_ts: the speed in x (forward > 0), y (right > 0), and z (down > 0). The ts is the timestamp that the speed was valid.
* altitude, altitude_ts: wifi only, altitude in meters. Zero is where you took off. The ts is the timestamp where the altitude was valid.
* quaternion_w, quaternion_x, quaternion_y, quaternion_z, quaternion_ts: wifi only. Quaternion as estimated from takeoff (which is set to 0). Ranges from -1 to 1. ts is the timestamp where this was valid.
* ``get_estimated_z_orientation()``: returns the estimated orientation using the unit quaternions. Note that 0 is the direction the drone is facing when you boot it up
* sensors_dict: all other sensors are saved by name in a dictionary. The names come from the `minidrone.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/minidrone.xml>`_ and `common.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/common.xml>`_.

View File

@ -0,0 +1,7 @@
pyparrot
========
.. toctree::
:maxdepth: 4
pyparrot

View File

@ -0,0 +1,30 @@
pyparrot.commandsandsensors package
===================================
Submodules
----------
pyparrot.commandsandsensors.DroneCommandParser module
-----------------------------------------------------
.. automodule:: pyparrot.commandsandsensors.DroneCommandParser
:members:
:undoc-members:
:show-inheritance:
pyparrot.commandsandsensors.DroneSensorParser module
----------------------------------------------------
.. automodule:: pyparrot.commandsandsensors.DroneSensorParser
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pyparrot.commandsandsensors
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,30 @@
pyparrot.networking package
===========================
Submodules
----------
pyparrot.networking.bleConnection module
----------------------------------------
.. automodule:: pyparrot.networking.bleConnection
:members:
:undoc-members:
:show-inheritance:
pyparrot.networking.wifiConnection module
-----------------------------------------
.. automodule:: pyparrot.networking.wifiConnection
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pyparrot.networking
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,64 @@
pyparrot package
================
Subpackages
-----------
.. toctree::
pyparrot.commandsandsensors
pyparrot.networking
pyparrot.scripts
pyparrot.utils
Submodules
----------
pyparrot.Bebop module
---------------------
.. automodule:: pyparrot.Bebop
:members:
:undoc-members:
:show-inheritance:
pyparrot.DroneVision module
---------------------------
.. automodule:: pyparrot.DroneVision
:members:
:undoc-members:
:show-inheritance:
pyparrot.DroneVisionGUI module
------------------------------
.. automodule:: pyparrot.DroneVisionGUI
:members:
:undoc-members:
:show-inheritance:
pyparrot.Minidrone module
---------------------
.. automodule:: pyparrot.Minidrone
:members:
:undoc-members:
:show-inheritance:
pyparrot.VisionServer module
----------------------------
.. automodule:: pyparrot.VisionServer
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pyparrot
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,22 @@
pyparrot.scripts package
========================
Submodules
----------
pyparrot.scripts.findMinidrone module
---------------------------------
.. automodule:: pyparrot.scripts.findMinidrone
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pyparrot.scripts
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,38 @@
pyparrot.utils package
======================
Submodules
----------
pyparrot.utils.NonBlockingStreamReader module
---------------------------------------------
.. automodule:: pyparrot.utils.NonBlockingStreamReader
:members:
:undoc-members:
:show-inheritance:
pyparrot.utils.colorPrint module
--------------------------------
.. automodule:: pyparrot.utils.colorPrint
:members:
:undoc-members:
:show-inheritance:
pyparrot.utils.vlc module
-------------------------
.. automodule:: pyparrot.utils.vlc
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pyparrot.utils
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,199 @@
.. title:: Quick Start with a Bebop
.. quickstartmambo:
Quick Start Guide with a Bebop
==============================
Using the pyparrot library on the Bebop
---------------------------------------
Before running any of the sample code, you will need to connect to your drone. To control the Bebop, you need to
connect your controlling device (laptop, computer, etc) to the wifi for the drone. Look for the wifi network
named Bebop_number where number varies for each drone.
Quick start: Demo Code
-----------------------
I have provided a set of `example <https://github.com/amymcgovern/pyparrot/tree/master/examples>`_ scripts for both the
Mambo and the Bebop.
Demo of the trick commands on the bebop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The code shown below is the
`demoBebopTricks.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopTricks.py>`_.
demoBebopTricks.py will take off, demonstrate all 4 types of flips, and then land. It is a good program to
verify that your connection to your bebop is working well. The bebop can flip just like the Mambo! This does
the exact same thing as the Mambo tricks demo: take off, flip in all 4 directions, land.
**Be sure to run it in a room large enough to perform the flips!**
.. code-block:: python
"""
Demos the tricks on the bebop. Make sure you have enough room to perform them!
Author: Amy McGovern
"""
from pyparrot.Bebop import Bebop
bebop = Bebop()
print("connecting")
success = bebop.connect(10)
print(success)
print("sleeping")
bebop.smart_sleep(5)
bebop.ask_for_state_update()
bebop.safe_takeoff(10)
print("flip left")
print("flying state is %s" % bebop.sensors.flying_state)
success = bebop.flip(direction="left")
print("mambo flip result %s" % success)
bebop.smart_sleep(5)
print("flip right")
print("flying state is %s" % bebop.sensors.flying_state)
success = bebop.flip(direction="right")
print("mambo flip result %s" % success)
bebop.smart_sleep(5)
print("flip front")
print("flying state is %s" % bebop.sensors.flying_state)
success = bebop.flip(direction="front")
print("mambo flip result %s" % success)
bebop.smart_sleep(5)
print("flip back")
print("flying state is %s" % bebop.sensors.flying_state)
success = bebop.flip(direction="back")
print("mambo flip result %s" % success)
bebop.smart_sleep(5)
bebop.smart_sleep(5)
bebop.safe_land(10)
print("DONE - disconnecting")
bebop.disconnect()
Outdoor or large area demo of the direct flight commands on the bebop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The second example program shows how to directly fly the bebop by controlling the yaw, pitch, roll, and
vertical movement parameters. **Make sure you try this one in a large enough room!**
This code is provided in
`demoBebopDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopDirectFlight.py>`_
and is also shown below.
.. code-block:: python
"""
Flies the bebop in a fairly wide arc. You want to be sure you have room for this. (it is commented
out but even what is here is still going to require a large space)
Author: Amy McGovern
"""
from pyparrot.Bebop import Bebop
bebop = Bebop()
print("connecting")
success = bebop.connect(10)
print(success)
print("sleeping")
bebop.smart_sleep(5)
bebop.ask_for_state_update()
bebop.safe_takeoff(10)
print("Flying direct: going forward (positive pitch)")
bebop.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
print("Flying direct: yaw")
bebop.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
print("Flying direct: going backwards (negative pitch)")
bebop.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
print("Flying direct: roll")
bebop.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
print("Flying direct: going up")
bebop.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
print("Turning relative")
bebop.move_relative(0, 0, 0, math.radians(90))
# this works but requires a larger test space than I currently have. Uncomment with care and test only in large spaces!
#print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
#bebop.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=5)
bebop.smart_sleep(5)
bebop.safe_land(10)
print("DONE - disconnecting")
bebop.disconnect()
Indoor demo of the direct flight commands on the bebop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you couldn't run the outdoor or large space demo to test your bebop, this one is designed for a smaller space.
It simply takes off, turns, and lands. **Make sure you are still flying in a safe place!** This code is provided in
`demoBebopIndoors.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopIndoors.py>`_
and is also shown below.
.. code-block:: python
"""
Demo the Bebop indoors (sets small speeds and then flies just a small amount)
Note, the bebop will hurt your furniture if it hits it. Even though this is a very small
amount of flying, be sure you are doing this in an open area and are prepared to catch!
Author: Amy McGovern
"""
from pyparrot.Bebop import Bebop
bebop = Bebop()
print("connecting")
success = bebop.connect(10)
print(success)
if (success):
print("turning on the video")
bebop.start_video_stream()
print("sleeping")
bebop.smart_sleep(2)
bebop.ask_for_state_update()
bebop.safe_takeoff(10)
# set safe indoor parameters
bebop.set_max_tilt(5)
bebop.set_max_vertical_speed(1)
# trying out the new hull protector parameters - set to 1 for a hull protection and 0 without protection
bebop.set_hull_protection(1)
print("Flying direct: Slow move for indoors")
bebop.fly_direct(roll=0, pitch=20, yaw=0, vertical_movement=0, duration=2)
bebop.smart_sleep(5)
bebop.safe_land(10)
print("DONE - disconnecting")
bebop.stop_video_stream()
bebop.smart_sleep(5)
bebop.disconnect()

View File

@ -0,0 +1,604 @@
.. title:: Quick Start with a Minidrone
.. quickstartmambo:
Quick Start Guide with a Minidrone
==============================
Using the pyparrot library on the Minidrone
---------------------------------------
Before running any of the sample code, you will need to connect to your drone. If you have a Mambo FPV, I highly
recommend using the wifi connection since it sends much more information using wifi than BLE. If you have a Mambo Code
or a Mambo Fly or Swing(neither of which has a camera), then you need to use the BLE connection.
wifi connection
^^^^^^^^^^^^^^^
If you are using the wifi (e.g. Mambo FPV), you need to connect your controlling device (laptop, computer, etc)
to the wifi for the drone. Look for a wifi network named Mambo_number where number changes for each drone.
BLE connection
^^^^^^^^^^^^^^
If you do not have a camera or want to use BLE for other reasons(e.g. swarm), you will first need to find the
BLE address of your Minidrone(s). BLE permissions on linux require that this command run in sudo mode.
To run this, from the bin directory for your python installation, type:
::
sudo findMinidrone
This will identify all BLE devices within hearing of the Pi. The Minidrone's specific address will be printed at the end.
Save the address and use it in your connection code (discussed below). If findMinidrone does not
report "FOUND A MAMBO!" or "FOUND A SWING!", then be sure your minidrone is turned on when you run the findMambo code and that your Pi
(or other linux box) has its BLE interface turned on.
The output should look something like this. I removed my own BLE addresses from my network for security but I am
showing the address of the mambo that I use for all the demo scripts.
.. code-block:: console
~/miniconda3/bin $ sudo ./find_mambo
Discovered device <address removed>
Discovered device <address removed>
Discovered device <address removed>
Discovered device e0:14:d0:63:3d:d0
Received new data from <address removed>
Discovered device <address removed>
Discovered device <address removed>
Received new data from <address removed>
Discovered device <address removed>
FOUND A MAMBO!
Device e0:14:d0:63:3d:d0 (random), RSSI=-60 dB
Complete Local Name = Mambo_<numbers>
Quick start: Demo Code
-----------------------
I have provided a set of `example <https://github.com/amymcgovern/pyparrot/tree/master/examples>`_ scripts for both the
Mambo and the Bebop. Note that you will need to edit the minidrone scripts to either use your own BLE address or to
ensure that use_wifi=True is set, so that it connects using wifi.
**Note that you do not need to run any of the other code in sudo mode!** That was only for discovery.
Demo of the trick commands on the mambo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The code shown below is the
`demoMamboTricks.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboTricks.py>`_.
demoMamboTricks.py will take off, demonstrate all 4 types of flips, and then land. It is a good program to
verify that your connection to your mambo is working well. Be sure to run it in a room large enough
to perform the flips! The highlighted lines need to change for YOUR mambo and connection choices.
.. code-block:: python
"""
Demo the trick flying for the python interface
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
# you will need to change this to the address of YOUR mambo
mamboAddr = "e0:14:d0:63:3d:d0"
# make my mambo object
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
mambo = Mambo(mamboAddr, use_wifi=True)
print("trying to connect")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
if (success):
# get the state information
print("sleeping")
mambo.smart_sleep(2)
mambo.ask_for_state_update()
mambo.smart_sleep(2)
print("taking off!")
mambo.safe_takeoff(5)
if (mambo.sensors.flying_state != "emergency"):
print("flying state is %s" % mambo.sensors.flying_state)
print("Flying direct: going up")
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=20, duration=1)
print("flip left")
print("flying state is %s" % mambo.sensors.flying_state)
success = mambo.flip(direction="left")
print("mambo flip result %s" % success)
mambo.smart_sleep(5)
print("flip right")
print("flying state is %s" % mambo.sensors.flying_state)
success = mambo.flip(direction="right")
print("mambo flip result %s" % success)
mambo.smart_sleep(5)
print("flip front")
print("flying state is %s" % mambo.sensors.flying_state)
success = mambo.flip(direction="front")
print("mambo flip result %s" % success)
mambo.smart_sleep(5)
print("flip back")
print("flying state is %s" % mambo.sensors.flying_state)
success = mambo.flip(direction="back")
print("mambo flip result %s" % success)
mambo.smart_sleep(5)
print("landing")
print("flying state is %s" % mambo.sensors.flying_state)
mambo.safe_land(5)
mambo.smart_sleep(5)
print("disconnect")
mambo.disconnect()
Demo of the direct flight commands on the mambo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The second example program shows how to directly fly the mambo by controlling the yaw, pitch, roll, and
vertical movement parameters. **Make sure you try this one in a large enough room!**
This code is provided in
`demoMamboDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboDirectFlight.py>`_
and is also shown below. Again, the highlighted lines must be changed to the parameters for your mambo and connection.
.. code-block:: python
"""
Demo the direct flying for the python interface
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
# you will need to change this to the address of YOUR mambo
mamboAddr = "e0:14:d0:63:3d:d0"
# make my mambo object
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
mambo = Mambo(mamboAddr, use_wifi=True)
print("trying to connect")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
if (success):
# get the state information
print("sleeping")
mambo.smart_sleep(2)
mambo.ask_for_state_update()
mambo.smart_sleep(2)
print("taking off!")
mambo.safe_takeoff(5)
print("Flying direct: going forward (positive pitch)")
mambo.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
print("Showing turning (in place) using turn_degrees")
mambo.turn_degrees(90)
mambo.smart_sleep(2)
mambo.turn_degrees(-90)
mambo.smart_sleep(2)
print("Flying direct: yaw")
mambo.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
print("Flying direct: going backwards (negative pitch)")
mambo.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
print("Flying direct: roll")
mambo.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
print("Flying direct: going up")
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
mambo.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=3)
print("landing")
mambo.safe_land(5)
mambo.smart_sleep(5)
print("disconnect")
mambo.disconnect()
Demo of the USB claw accessory
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If your mambo has the USB accessories (claw and gun), you can control them but you *MUST* be in BLE mode.
The mambo can only handle one USB accessory at a time and the camera counts as a USB accessory so you must use
the BLE connection only. `demoMamboClaw.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboClaw.py>`_
show how to use the claw accessory. The highlighted line must be changed to the BLE address for your mambo and the use_wifi
parameter must stay at False. In this demo program, the mambo takes off, opens and closes the claw, and lands again.
.. code-block:: python
"""
Demo the claw for the python interface
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
# you will need to change this to the address of YOUR mambo
mamboAddr = "e0:14:d0:63:3d:d0"
# make my mambo object
# remember you can't use the claw with the camera installed so this must be BLE connected to work
mambo = Mambo(mamboAddr, use_wifi=False)
print("trying to connect")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
# get the state information
print("sleeping")
mambo.smart_sleep(2)
mambo.ask_for_state_update()
mambo.smart_sleep(2)
print("taking off!")
mambo.safe_takeoff(5)
print("open and close the claw")
mambo.open_claw()
# you have to sleep to let the claw open (it needs time to do it)
mambo.smart_sleep(5)
mambo.close_claw()
# you have to sleep to let the claw close (it needs time to do it)
mambo.smart_sleep(5)
print("landing")
mambo.safe_land(5)
mambo.smart_sleep(5)
print("disconnect")
mambo.disconnect()
Demo of the USB gun accessory
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`demoMamboGun.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboGun.py>`_
show how to use the gun accessory. The highlighted line must be changed to the BLE address for your mambo and the use_wifi
parameter must stay at False. In this demo program, the mambo takes off, fires the gun, and lands again.
.. code-block:: python
"""
Demo the gun for the python interface
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
# you will need to change this to the address of YOUR mambo
mamboAddr = "e0:14:d0:63:3d:d0"
# make my mambo object
# remember you can't use the gun with the camera installed so this must be BLE connected to work
mambo = Mambo(mamboAddr, use_wifi=False)
print("trying to connect")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
# get the state information
print ("sleeping")
mambo.smart_sleep(2)
mambo.ask_for_state_update()
mambo.smart_sleep(2)
print("shoot the gun")
mambo.fire_gun()
# sleep to ensure it does the firing
mambo.smart_sleep(15)
print("disconnect")
mambo.disconnect()
Demo of the ground-facing camera
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`demoMamboGroundcam.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboGroundcam.py>`_
show how to use the mambo's ground-facing camera. This feature **ONLY** works in wifi mode. It can be slow
to download the frames so do not count on this running at several frames per second. The example code shown
below takes off, takes a picture, and then grabs a random picture from the ground facing camera set.
.. code-block:: python
"""
Demo of the groundcam
Mambo takes off, takes a picture and shows a RANDOM frame, not the last one
Author: Valentin Benke, https://github.com/Vabe7
Author: Amy McGovern
"""
from pyparrot.Minidrone import Mambo
import cv2
mambo = Mambo(None, use_wifi=True) #address is None since it only works with WiFi anyway
print("trying to connect to mambo now")
success = mambo.connect(num_retries=3)
print("connected: %s" % success)
if (success):
# get the state information
print("sleeping")
mambo.smart_sleep(1)
mambo.ask_for_state_update()
mambo.smart_sleep(1)
mambo.safe_takeoff(5)
# take the photo
pic_success = mambo.take_picture()
# need to wait a bit for the photo to show up
mambo.smart_sleep(0.5)
picture_names = mambo.groundcam.get_groundcam_pictures_names() #get list of availible files
print(picture_names)
frame = mambo.groundcam.get_groundcam_picture(picture_names[0],True) #get frame which is the first in the array
if frame is not None:
if frame is not False:
cv2.imshow("Groundcam", frame)
cv2.waitKey(100)
mambo.safe_land(5)
mambo.disconnect()
Demo of the flying mode on the swing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`demoSwingDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoSwingDirectFlight.py>`_
You can see how to use the set_flying_mode command. I advise you to have enough space to use this script.
.. code-block:: python
"""
Demo the direct flying for the python interface
Author: Victor804
"""
from pyparrot.Minidrone import Swing
# you will need to change this to the address of YOUR swing
swingAddr = "e0:14:04:a7:3d:cb"
# make my swing object
swing = Swing(swingAddr)
print("trying to connect")
success = swing.connect(num_retries=3)
print("connected: %s" % success)
if (success):
# get the state information
print("sleeping")
swing.smart_sleep(2)
swing.ask_for_state_update()
swing.smart_sleep(2)
print("taking off!")
swing.safe_takeoff(5)
print("plane forward")
swing.set_flying_mode("plane_forward")
swing.smart_sleep(1)
print("quadricopter")
swing.set_flying_mode("quadricopter")
print("landing")
swing.safe_land(5)
swing.smart_sleep(5)
print("disconnect")
swing.disconnect()
Demo joystick for Swing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`demoSwingJoystick.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoSwingJoystick.py>`_
Example code to control the swig with a joystick. Easy to modify for your needs.
.. code-block:: python
import pygame
import sys
from pyparrot.Minidrone import Swing
def joystick_init():
"""
Initializes the controller, allows the choice of the controller.
If no controller is detected returns an error.
:param:
:return joystick:
"""
pygame.init()
pygame.joystick.init()
joystick_count = pygame.joystick.get_count()
if joystick_count > 0:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()
name = joystick.get_name()
print([i], name)
joystick.quit()
else:
sys.exit("Error: No joystick detected")
selected_joystick = eval(input("Enter your joystick number:"))
if selected_joystick not in range(joystick_count):
sys.exit("Error: Your choice is not valid")
joystick = pygame.joystick.Joystick(selected_joystick)
joystick.init()
return joystick
def mapping_button(joystick, dict_commands):
"""
Associating a controller key with a command in dict_commands.
:param joystick, dict_commands:
:return mapping:
"""
mapping = {}
for command in dict_commands:
print("Press the key", command)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.JOYBUTTONDOWN:
if event.button not in (value for value in mapping.values()):
mapping[command] = event.button
done = True
return mapping
def mapping_axis(joystick, axes=["pitch", "roll", "yaw", "vertical"]):
"""
Associating the analog thumbsticks of the controller with a command in dict commands
:param joystick, dict_commands:
:return mapping:
"""
mapping = {}
for i in axes:
print("Push the", i, "axis")
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.JOYAXISMOTION:
if event.axis not in (value for value in mapping.values()):
mapping[i] = event.axis
done = True
return mapping
def _parse_button(dict_commands, button):
"""
Send the commands to the drone.
If multiple commands are assigned to a key each command will be sent one by one to each press.
:param dict_commands, button:
:return:
"""
commands = dict_commands[button][0]
args = dict_commands[button][-1]
command = commands[0]
arg = args[0]
if len(commands) == 1:
if len(args) == 1:
command(arg)
else:
command(arg)
dict_commands[button][-1] = args[1:]+[arg]
else:
if len(commands) == 1:
command(arg)
dict_commands[button][0] = commands[1:]+[command]
else:
command(arg)
dict_commands[button][0] = commands[1:]+[command]
dict_commands[button][-1] = args[1:]+[arg]
def main_loop(joystick, dict_commands, mapping_button, mapping_axis):
"""
First connects to the drone and makes a flat trim.
Then in a loop read the events of the controller to send commands to the drone.
:param joystick, dict_commands, mapping_button, mapping_axis:
:return:
"""
swing.connect(10)
swing.flat_trim()
while True:
pygame.event.get()
pitch = joystick.get_axis(mapping_axis["pitch"])*-100
roll = joystick.get_axis(mapping_axis["roll"])*100
yaw = joystick.get_axis(mapping_axis["yaw"])*100
vertical = joystick.get_axis(mapping_axis["vertical"])*-100
swing.fly_direct(roll, pitch, yaw, vertical, 0.1)
for button, value in mapping_button.items():
if joystick.get_button(value):
_parse_button(dict_commands, button)
if __name__ == "__main__":
swing = Swing("e0:14:04:a7:3d:cb")
#Example of dict_commands
dict_commands = {
"takeoff_landing":[ #Name of the button
[swing.safe_takeoff, swing.safe_land],#Commands execute one by one
[5]#Argument for executing the function
],
"fly_mode":[
[swing.set_flying_mode],
["quadricopter", "plane_forward"]
],
"plane_gear_box_up":[
[swing.set_plane_gear_box],
[((swing.sensors.plane_gear_box[:-1]+str(int(swing.sensors.plane_gear_box[-1])+1)) if swing.sensors.plane_gear_box[-1] != "3" else "gear_3")]#"gear_1" => "gear_2" => "gear_3"
],
"plane_gear_box_down":[
[swing.set_plane_gear_box],
[((swing.sensors.plane_gear_box[:-1]+str(int(swing.sensors.plane_gear_box[-1])-1)) if swing.sensors.plane_gear_box[-1] != "1" else "gear_1")]#"gear_3" => "gear_2" => "gear_1"
]
}
joystick = joystick_init()
mapping_button = mapping_button(joystick, dict_commands)
mapping_axis = mapping_axis(joystick)
main_loop(joystick, dict_commands, mapping_button, mapping_axis)

Some files were not shown because too many files have changed in this diff Show More