├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── example ├── builtin_switch.py ├── data_read.py ├── example.py ├── heartbeat_func.py ├── mqtt_msg.py ├── notice.py ├── realtime.py ├── storage_log.py ├── storage_obj.py ├── storage_text.py ├── storage_ts.py ├── timing_and_countdown.py ├── voice_assistant.py └── weather.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── blinker │ ├── __init__.py │ ├── device.py │ ├── errors.py │ ├── httpclient.py │ ├── mqttclient.py │ ├── voice_assistant.py │ └── widget.py └── version.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: auto-deploy 2 | on: 3 | push: 4 | branches: [dev_3.0] 5 | 6 | jobs: 7 | archive-file: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: get version 12 | id: version 13 | uses: notiz-dev/github-action-json-property@release 14 | with: 15 | path: 'version.json' 16 | prop_path: 'version' 17 | 18 | - name: get current time 19 | uses: 1466587594/get-current-time@v2 20 | id: current-time 21 | with: 22 | format: YYYY.MM.DD 23 | utcOffset: "+08:00" 24 | 25 | - name: action-zip 26 | uses: montudor/action-zip@v0.1.1 27 | with: 28 | args: zip -qq -r blinker-py-${{steps.version.outputs.prop}}.zip . -x .github/* -x .git/* 29 | 30 | - name: create json 31 | id: jsonfile 32 | uses: jsdaniell/create-json@1.1.2 33 | with: 34 | name: "python.json" 35 | json: '{"img": "assets/sdk/python.png", "text": "Python", "update": "${{ steps.current-time.outputs.formattedTime}}", "version": "${{steps.version.outputs.prop}}", "github": "https://github.com/blinker-iot/blinker-py", "document": "https://diandeng.tech/doc/python-support", "download": "sdk/blinker-py-${{steps.version.outputs.prop}}.zip" }' 36 | 37 | - name: upload zip 38 | uses: garygrossgarten/github-action-scp@release 39 | with: 40 | local: blinker-py-${{steps.version.outputs.prop}}.zip 41 | remote: /home/ubuntu/www/diandeng.tech/sdk/blinker-py-${{steps.version.outputs.prop}}.zip 42 | host: ${{ secrets.SERVER_IP }} 43 | username: ubuntu 44 | password: ${{ secrets.SERVER_PASSWD }} 45 | recursive: true 46 | 47 | - name: upload json 48 | uses: garygrossgarten/github-action-scp@release 49 | with: 50 | local: python.json 51 | remote: /home/ubuntu/www/diandeng.tech/sdk/python.json 52 | host: ${{ secrets.SERVER_IP }} 53 | username: ubuntu 54 | password: ${{ secrets.SERVER_PASSWD }} 55 | recursive: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __vm/ 2 | Debug/ 3 | *.vcxproj 4 | *.filters 5 | .vs/ 6 | *.sln 7 | .vscode/ 8 | *.pyc 9 | Blinker/__pycache__ 10 | .idea/ 11 | dist/ 12 | *.egg-info 13 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 i3water 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blinker-py 2 | Blinker python library for hardware. Works with Raspberry Pi, Banana Pi, linux. 3 | 4 | read more: https://doc.blinker.app -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | # blinker-py 2 | Blinker python library for hardware. Works with Raspberry Pi, Banana Pi, linux. 3 | 4 | read more: https://doc.blinker.app -------------------------------------------------------------------------------- /example/builtin_switch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from blinker import Device 9 | 10 | 11 | async def builtin_switch_func(msg): 12 | print("builtinSwitch: {0}".format(msg)) 13 | if msg["switch"] == "on": 14 | await device.builtinSwitch.set_state("on").update() 15 | else: 16 | await device.builtinSwitch.set_state("off").update() 17 | 18 | 19 | device = Device("authKey", builtin_switch_func=builtin_switch_func) 20 | 21 | if __name__ == '__main__': 22 | device.run() 23 | -------------------------------------------------------------------------------- /example/data_read.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | mqtt其它数据处理 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | 11 | 12 | async def ready_func(): 13 | print(device.data_reader.get()) 14 | 15 | 16 | device = Device("authKey", ready_func=ready_func) 17 | 18 | if __name__ == '__main__': 19 | device.run() 20 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "stao" 5 | 6 | from blinker import Device, ButtonWidget, NumberWidget 7 | 8 | device = Device("authKey") 9 | 10 | button1 = device.addWidget(ButtonWidget('btn-123')) 11 | button2 = device.addWidget(ButtonWidget('btn-abc')) 12 | number1 = device.addWidget(NumberWidget('num-abc')) 13 | 14 | num = 0 15 | 16 | 17 | async def button1_callback(msg): 18 | global num 19 | 20 | num += 1 21 | 22 | await number1.text("num").value(num).update() 23 | 24 | 25 | async def button2_callback(msg): 26 | print("Button2: {0}".format(msg)) 27 | 28 | 29 | async def heartbeat_func(msg): 30 | print("Heartbeat func received: {0}".format(msg)) 31 | # 文本组件 32 | 33 | 34 | async def ready_func(): 35 | # 获取设备配置信息 36 | print(vars(device.config)) 37 | 38 | 39 | button1.func = button1_callback 40 | button2.func = button2_callback 41 | 42 | device.heartbeat_callable = heartbeat_func 43 | device.ready_callable = ready_func 44 | 45 | if __name__ == '__main__': 46 | device.run() 47 | -------------------------------------------------------------------------------- /example/heartbeat_func.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from blinker import Device 9 | 10 | 11 | async def heartbeat_func(msg): 12 | print("Heartbeat received msg: {0}".format(msg)) 13 | 14 | 15 | device = Device("authKey", heartbeat_func=heartbeat_func) 16 | 17 | if __name__ == '__main__': 18 | device.run() 19 | -------------------------------------------------------------------------------- /example/mqtt_msg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | MQTT消息发送 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | 11 | 12 | async def ready_func(): 13 | msg = {"abc": 123} 14 | to_device = "设备名" 15 | await device.sendMessage(msg, to_device) 16 | 17 | 18 | device = Device("authKey", protocol="mqtts", ready_func=ready_func) 19 | 20 | if __name__ == '__main__': 21 | device.run() 22 | -------------------------------------------------------------------------------- /example/notice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 通知 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | 11 | 12 | async def ready_func(): 13 | await device.sendSms("test") 14 | await device.wechat(title="消息测试", state="异常", text="设备1出现异常") 15 | 16 | 17 | device = Device("authKey", protocol="mqtts", ready_func=ready_func) 18 | 19 | if __name__ == '__main__': 20 | device.run() 21 | -------------------------------------------------------------------------------- /example/realtime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 实时数据 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | import random 10 | 11 | from blinker import Device 12 | 13 | 14 | def generate_data(): 15 | return random.randint(1, 100) 16 | 17 | 18 | async def realtime_func(keys): 19 | print("realtime func received {0}".format(keys)) 20 | for key in keys: 21 | if key == "humi": 22 | await device.sendRtData(key, generate_data) 23 | elif key == "temp": 24 | await device.sendRtData(key, generate_data) 25 | 26 | 27 | device = Device("authKey", realtime_func=realtime_func) 28 | 29 | if __name__ == '__main__': 30 | device.run() 31 | -------------------------------------------------------------------------------- /example/storage_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 日志存储示例 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | 11 | 12 | async def ready_func(): 13 | while True: 14 | log = "This is log test" 15 | await device.saveLogData(log) 16 | 17 | 18 | device = Device("authKey", protocol="mqtts", ready_func=ready_func) 19 | 20 | if __name__ == '__main__': 21 | device.run() 22 | -------------------------------------------------------------------------------- /example/storage_obj.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from blinker import Device 9 | 10 | 11 | async def ready_func(): 12 | await device.saveObjectData({"blinker": "nice"}) 13 | 14 | 15 | device = Device("authKey", protocol="mqtts", ready_func=ready_func) 16 | 17 | if __name__ == '__main__': 18 | device.run() 19 | -------------------------------------------------------------------------------- /example/storage_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from blinker import Device 9 | 10 | 11 | async def ready_func(): 12 | await device.saveTextData("hello, blinker") 13 | 14 | 15 | device = Device("authKey", ready_func=ready_func) 16 | 17 | if __name__ == '__main__': 18 | device.run() 19 | -------------------------------------------------------------------------------- /example/storage_ts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | import random 9 | 10 | from blinker import Device 11 | 12 | 13 | async def ready_func(): 14 | while True: 15 | print("save ts data...") 16 | data = { 17 | "humi": random.randint(0, 300), 18 | "temp": random.randint(0, 100) 19 | } 20 | 21 | await device.saveTsData(data) 22 | 23 | 24 | device = Device("authKey", protocol="mqtts", ready_func=ready_func) 25 | 26 | if __name__ == '__main__': 27 | device.run() 28 | -------------------------------------------------------------------------------- /example/timing_and_countdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 定时和倒计时 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | 11 | 12 | async def builtin_switch_func(msg): 13 | print("received msg: {0}".format(msg)) 14 | 15 | 16 | device = Device("authKey", builtin_switch_func=builtin_switch_func) 17 | 18 | if __name__ == '__main__': 19 | device.run() 20 | -------------------------------------------------------------------------------- /example/voice_assistant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 天猫精灵接入 5 | """ 6 | 7 | __author__ = 'stao' 8 | 9 | from blinker import Device 10 | from blinker.voice_assistant import VoiceAssistant, VAType, AliLightMode, PowerMessage, ModeMessage, ColorMessage, \ 11 | ColorTempMessage, BrightnessMessage, DataMessage 12 | 13 | 14 | async def power_change(message: PowerMessage): 15 | """ 电源状态改变(适用于灯和插座) 16 | """ 17 | 18 | set_state = message.data["pState"] 19 | print("change power state to : {0}".format(set_state)) 20 | 21 | if set_state == "on": 22 | pass 23 | elif set_state == "off": 24 | pass 25 | 26 | await (await message.power(set_state)).update() 27 | 28 | 29 | async def mode_change(message: ModeMessage): 30 | """ 模式改变(适用于灯和插座) 31 | """ 32 | 33 | mode = message.data["mode"] 34 | print("change mode to {0}".format(mode)) 35 | 36 | if mode == AliLightMode.READING: 37 | pass 38 | elif mode == AliLightMode.MOVIE: 39 | pass 40 | elif mode == AliLightMode.SLEEP: 41 | pass 42 | elif mode == AliLightMode.HOLIDAY: 43 | pass 44 | elif mode == AliLightMode.MUSIC: 45 | pass 46 | elif mode == AliLightMode.COMMON: 47 | pass 48 | 49 | await (await message.mode(mode)).update() 50 | 51 | 52 | async def color_change(message: ColorMessage): 53 | """ 颜色改变(适用于灯) 54 | 支持的颜色:Red红色\Yellow黄色\Blue蓝色\Green绿色\White白色\Black黑色\Cyan青色\Purple紫色\Orange橙色 55 | """ 56 | 57 | color = message.data["col"] 58 | print("change color to {0}".format(color)) 59 | await (await message.color(color)).update() 60 | 61 | 62 | async def colorTemp_change(message: ColorTempMessage): 63 | """色温改变(适用于灯) 64 | """ 65 | 66 | color_temp = message.data["colTemp"] 67 | print("change color temp to {0}".format(color_temp)) 68 | await (await message.colorTemp(100)).update() 69 | 70 | 71 | async def brightness_change(message: BrightnessMessage): 72 | """ 亮度改变(适用于灯) 73 | """ 74 | 75 | if "bright" in message.data: 76 | brightness = int(message.data["bright"]) 77 | elif "upBright" in message.data: 78 | brightness = int(message.data["upBright"]) 79 | elif "downBright" in message.data: 80 | brightness = int(message.data["downBright"]) 81 | else: 82 | brightness = 50 83 | 84 | print("change brightness to {0}".format(brightness)) 85 | await (await message.brightness(brightness)).update() 86 | 87 | 88 | async def state_query(message: DataMessage): 89 | print("query state: {0}".format(message.data)) 90 | await message.power("on") 91 | await message.mode(AliLightMode.HOLIDAY) 92 | await message.color("red") 93 | await message.brightness(66) 94 | await message.update() 95 | 96 | 97 | device = Device("authKey", ali_type=VAType.LIGHT) 98 | voice_assistant = VoiceAssistant(VAType.LIGHT) 99 | voice_assistant.mode_change_callable = mode_change 100 | voice_assistant.colortemp_change_callable = colorTemp_change 101 | voice_assistant.color_change_callable = color_change 102 | voice_assistant.brightness_change_callable = brightness_change 103 | voice_assistant.state_query_callable = state_query 104 | voice_assistant.power_change_callable = power_change 105 | 106 | device.addVoiceAssistant(voice_assistant) 107 | 108 | if __name__ == '__main__': 109 | device.run() 110 | -------------------------------------------------------------------------------- /example/weather.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from blinker import Device 9 | 10 | 11 | async def ready_func(): 12 | print(await device.getWeather()) 13 | print(await device.getAir()) 14 | print(await device.getWeatherForecast(510100)) 15 | 16 | 17 | device = Device("authKey") 18 | device.ready_callable = ready_func 19 | 20 | if __name__ == '__main__': 21 | device.run() 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp~=3.7.4 2 | certifi~=2021.10.8 3 | loguru~=0.5.3 4 | paho-mqtt~=1.6.1 5 | Rx~=3.2.0 6 | websockets~=10.2 7 | apscheduler~=3.9.1 8 | getmac~=0.8.3 9 | zeroconf~=0.38.4 10 | requests~=2.27.1 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = blinker-py 3 | version = 0.3.0 4 | author = stao 5 | author_email = werewolf_st@hotmail.com 6 | description = Blinker library in python 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/blinker-iot/blinker-py 10 | project_urls = 11 | Bug Tracker = https://github.com/blinker-iot/blinker-py/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | 17 | [options] 18 | package_dir = 19 | = src 20 | packages = find: 21 | python_requires = >=3.6 22 | install_requires = 23 | aiohttp~=3.7.4 24 | certifi~=2021.10.8 25 | loguru~=0.5.3 26 | paho-mqtt~=1.6.1 27 | Rx~=3.2.0 28 | websockets~=10.2 29 | apscheduler~=3.9.1 30 | getmac~=0.8.3 31 | zeroconf~=0.38.4 32 | requests~=2.27.1 33 | 34 | [options.packages.find] 35 | where = src -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "stao" 5 | 6 | import setuptools 7 | 8 | setuptools.setup( 9 | name="blinker-py", 10 | version="0.3.0", 11 | author="stao", 12 | author_email="werewolf_st@hotmail.com", 13 | description="Blinker library in python", 14 | long_description=open('README.md').read(), 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/blinker-iot/blinker-py", 17 | project_urls={ 18 | "Bug Tracker": "https://github.com/blinker-iot/blinker-py/issues", 19 | }, 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | package_dir={"": "src"}, 26 | packages=setuptools.find_packages(where="src"), 27 | python_requires=">=3.6", 28 | install_requires=[ 29 | 'aiohttp~=3.7.4', 30 | 'certifi~=2021.10.8', 31 | 'loguru~=0.5.3', 32 | 'paho-mqtt~=1.6.1', 33 | 'Rx~=3.2.0', 34 | "websockets~=10.2", 35 | 'apscheduler~=3.9.1', 36 | 'getmac~=0.8.3', 37 | 'zeroconf~=0.38.4', 38 | 'requests~=2.27.1', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /src/blinker/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "stao" 5 | 6 | from .device import * 7 | from .widget import * 8 | -------------------------------------------------------------------------------- /src/blinker/device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "stao" 5 | 6 | import asyncio 7 | import datetime 8 | import json 9 | import platform 10 | import math 11 | import os 12 | import threading 13 | import time 14 | import websockets 15 | 16 | from typing import Dict 17 | 18 | from asyncio.coroutines import iscoroutinefunction 19 | from apscheduler.schedulers.background import BackgroundScheduler 20 | from getmac import get_mac_address 21 | from zeroconf import ServiceInfo, Zeroconf 22 | from threading import Event 23 | from queue import SimpleQueue 24 | 25 | from loguru import logger 26 | 27 | from .httpclient import * 28 | from .mqttclient import * 29 | from .errors import * 30 | 31 | if platform.system() == "Windows": 32 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 33 | 34 | __all__ = ["Device"] 35 | 36 | 37 | class BuiltinSwitch: 38 | device = None 39 | key = "switch" 40 | state = "" 41 | 42 | def __init__(self, device, func=None): 43 | self.device = device 44 | self._func = func 45 | 46 | @property 47 | def func(self): 48 | return self._func 49 | 50 | @func.setter 51 | def func(self, func): 52 | self._func = func 53 | 54 | async def handler(self, msg): 55 | if self.func: 56 | if iscoroutinefunction(self._func): 57 | await self._func(msg) 58 | else: 59 | self._func(msg) 60 | else: 61 | logger.warning("Not setting callable func for {0}".format(self.key)) 62 | 63 | def set_state(self, state): 64 | self.state = state 65 | return self 66 | 67 | async def update(self): 68 | message = {self.key: self.state} 69 | self.device.mqtt_client.send_to_device(message) 70 | 71 | 72 | class DeviceConf(object): 73 | def __init__(self, **kw): 74 | self.__dict__.update(kw) 75 | 76 | 77 | class Device(object): 78 | """ 79 | :param auth_key 80 | :param version 81 | :param protocol 82 | :param ali_type 83 | :param duer_type 84 | :param mi_type 85 | :param heartbeat_func 86 | :param ready_func 87 | """ 88 | 89 | mqtt_client = None 90 | http_client = HttpClient() 91 | config: DeviceConf = None 92 | widgets = {} 93 | target_device = None 94 | shared_user_list = [] 95 | cache_data = None 96 | 97 | voice_assistant = None 98 | 99 | received_data = SimpleQueue() 100 | data_reader = SimpleQueue() 101 | 102 | builtinSwitch: BuiltinSwitch = None 103 | 104 | realtime_tasks = {} 105 | 106 | scheduler = BackgroundScheduler(timezone="Asia/Shanghai") 107 | countdown_timer = None 108 | countdown_timer2 = None 109 | 110 | auth_finished = Event() 111 | auth_finished.clear() 112 | mqtt_connected = Event() 113 | mqtt_connected.clear() 114 | 115 | # timing_tasks = TimingTasks() 116 | 117 | def __init__(self, auth_key, protocol: str = "mqtt", websocket: bool = True, source_check: bool = False, 118 | version: str = "1.0", ali_type: str = None, duer_type: str = None, mi_type: str = None, 119 | heartbeat_func=None, realtime_func=None, ready_func=None, builtin_switch_func=None): 120 | self.auth_key = auth_key 121 | self.version = version 122 | self.protocol = protocol 123 | self.ali_type = ali_type 124 | self.duer_type = duer_type 125 | self.mi_type = mi_type 126 | 127 | self._source_check = source_check 128 | self.websocket = websocket 129 | self.temp_data_path = "" 130 | self.temp_data = {} 131 | 132 | self._heartbeat_callable = heartbeat_func 133 | self._ready_callable = ready_func 134 | self._realtime_callable = realtime_func 135 | self._builtin_switch_callable = builtin_switch_func 136 | 137 | @property 138 | def heartbeat_callable(self): 139 | return self._heartbeat_callable 140 | 141 | @heartbeat_callable.setter 142 | def heartbeat_callable(self, func): 143 | self._heartbeat_callable = func 144 | 145 | @property 146 | def ready_callable(self): 147 | return self._ready_callable 148 | 149 | @ready_callable.setter 150 | def ready_callable(self, func): 151 | self._ready_callable = func 152 | 153 | @property 154 | def realtime_callable(self): 155 | return self._realtime_callable 156 | 157 | @realtime_callable.setter 158 | def realtime_callable(self, func): 159 | self._ready_callable = func 160 | 161 | @property 162 | def builtin_switch_callable(self): 163 | return self._builtin_switch_callable 164 | 165 | @builtin_switch_callable.setter 166 | def builtin_switch_callable(self, func): 167 | self._builtin_switch_callable = func 168 | 169 | def scheduler_run(self): 170 | self.scheduler.start() 171 | 172 | try: 173 | # This is here to simulate application activity (which keeps the main thread alive). 174 | while True: 175 | time.sleep(2) 176 | except (KeyboardInterrupt, SystemExit): 177 | # Not strictly necessary if daemonic mode is enabled but should be done if possible 178 | self.scheduler.shutdown() 179 | 180 | async def _custom_runner(self, func, **kwargs): 181 | self.auth_finished.wait() 182 | self.mqtt_connected.wait() 183 | 184 | if iscoroutinefunction(func): 185 | await func(**kwargs) 186 | else: 187 | func(**kwargs) 188 | 189 | def addWidget(self, widget): 190 | widget.device = self 191 | self.widgets[widget.key] = widget 192 | return widget 193 | 194 | def addVoiceAssistant(self, voice_assistant): 195 | voice_assistant.device = self 196 | self.voice_assistant = voice_assistant 197 | return voice_assistant 198 | 199 | async def device_init(self): 200 | broker_info = await self.http_client.diy_device_auth( 201 | self.auth_key, 202 | self.protocol, 203 | self.version, 204 | self.ali_type, 205 | self.duer_type, 206 | self.mi_type 207 | ) 208 | 209 | self.config = DeviceConf(**broker_info) 210 | 211 | share_info_res = await self.http_client.get_share_info() 212 | self.shared_user_list = share_info_res["users"] 213 | 214 | # 初始化内置开关 215 | self.builtinSwitch = BuiltinSwitch(self) 216 | self.builtinSwitch.func = self._builtin_switch_callable 217 | self.addWidget(self.builtinSwitch) 218 | 219 | # 加载缓存数据 220 | self.temp_data_path = f".{self.config.deviceName}.json" 221 | self.temp_data = await self.load_json_file() 222 | await self.load_timing_task() 223 | 224 | self.auth_finished.set() 225 | logger.success("Device auth successful...") 226 | 227 | async def load_json_file(self): 228 | if os.path.exists(self.temp_data_path): 229 | with open(self.temp_data_path) as f: 230 | return json.load(f) 231 | else: 232 | return {} 233 | 234 | def save_json_file(self, data): 235 | with open(self.temp_data_path, "w") as f: 236 | json.dump(data, f) 237 | 238 | async def mqttclient_init(self): 239 | self.auth_finished.wait() 240 | self.mqtt_client = MqttClient(self) 241 | await self.mqtt_client.connection() 242 | 243 | async def _cloud_heartbeat(self): 244 | """云端心跳上报 """ 245 | 246 | self.auth_finished.wait() 247 | while True: 248 | logger.info("Send cloud heartbeat") 249 | await self.http_client.cloud_heartbeat() 250 | await asyncio.sleep(600) 251 | 252 | async def sendMessage(self, message: Dict, to_device: str): 253 | self.mqtt_client.send_to_device(message, to_device) 254 | 255 | async def set_position(self, lng, lat): 256 | await self.http_client.set_position(lng, lat) 257 | 258 | def vibrate(self, t=500): 259 | self.mqtt_client.send_to_device({"vibrate": t}) 260 | 261 | def del_timing_task(self, task_id): 262 | self.disable_timing_task(task_id) 263 | 264 | def disable_timing_task(self, task_id): 265 | self.temp_data["timing"][task_id]["ena"] = 0 266 | self.scheduler.remove_job(f'timing-{task_id}') 267 | 268 | self.save_json_file(self.temp_data) 269 | 270 | async def load_timing_task(self): 271 | if "timing" not in self.temp_data: 272 | return 273 | 274 | for task in self.temp_data["timing"]: 275 | if task["ena"] == 1: 276 | await self.add_timing_task(task) 277 | 278 | def _execution_timing_task(self, task_data): 279 | self.received_data.put(task_data["act"][0]) 280 | if task_data["day"] == "0000000": 281 | self.disable_timing_task(task_data["task"]) 282 | self.mqtt_client.send_to_device(self.get_timing_data()) 283 | 284 | def get_timing_data(self): 285 | if "timing" not in self.temp_data: 286 | return {"timing": []} 287 | else: 288 | return {"timing": self.temp_data["timing"]} 289 | 290 | async def set_timing_data(self, data): 291 | if "timing" not in self.temp_data: 292 | self.temp_data["timing"] = [] 293 | index = 0 294 | else: 295 | index = data[0]["task"] 296 | if index < len(self.temp_data["timing"]): 297 | self.temp_data["timing"][index] = data[0] 298 | else: 299 | self.temp_data["timing"].append(data[0]) 300 | 301 | await self.add_timing_task(data[0]) 302 | 303 | async def add_timing_task(self, task_data): 304 | if task_data["ena"] == 0: 305 | self.disable_timing_task(task_data["task"]) 306 | else: 307 | hour = math.floor(task_data["tim"] / 60) 308 | minute = task_data["tim"] % 60 309 | 310 | day_of_week = [] 311 | for i in range(len(task_data["day"])): 312 | if task_data["day"][i] == "1": 313 | day_of_week.append((str(i))) 314 | 315 | conf = {"minute": minute, "hour": hour} 316 | if day_of_week: 317 | conf["day_of_week"] = ",".join(day_of_week) 318 | 319 | self.scheduler.add_job(self._execution_timing_task, "cron", **conf, args=[task_data], 320 | id=f'timing-{task_data["task"]}') 321 | 322 | async def del_timing_data(self, task_id): 323 | self.del_timing_task(task_id) 324 | del self.temp_data["timing"][task_id] 325 | for index in range(len(self.temp_data["timing"])): 326 | if index >= task_id: 327 | self.temp_data["timing"][index]["task"] = index 328 | index += 1 329 | 330 | self.save_json_file(self.temp_data) 331 | 332 | def get_countdown_data(self): 333 | if "countdown" not in self.temp_data: 334 | return {"countdown": False} 335 | else: 336 | return {"countdown": self.temp_data["countdown"]} 337 | 338 | def _countdown_func(self): 339 | if "countdown" in self.temp_data and not isinstance(self.temp_data["countdown"], bool): 340 | self.temp_data["countdown"]["rtim"] += 1 341 | self.mqtt_client.send_to_device(self.get_countdown_data()) 342 | 343 | if self.temp_data["countdown"]["rtim"] == self.temp_data["countdown"]["ttim"]: 344 | self.received_data.put(self.temp_data["countdown"]["act"][0]) 345 | self.temp_data["countdown"] = False 346 | self.mqtt_client.send_to_device(self.get_countdown_data()) 347 | 348 | async def clear_countdown_job(self): 349 | if self.countdown_timer: 350 | self.countdown_timer.remove() 351 | 352 | async def set_countdown_data(self, data): 353 | if data == "dlt": 354 | # 删除倒计时 355 | self.temp_data["countdown"] = False 356 | await self.clear_countdown_job() 357 | elif "run" in data and self.countdown_timer: 358 | if "countdown" not in self.temp_data: 359 | self.temp_data["countdown"] = {} 360 | self.temp_data["countdown"]["run"] = data["run"] 361 | if self.temp_data["countdown"]["run"] == 0: 362 | # 暂停倒计时 363 | self.countdown_timer.pause() 364 | elif self.temp_data["countdown"]["run"] == 1: 365 | # 重启倒计时 366 | self.countdown_timer.resume() 367 | else: 368 | # 设置倒计时 369 | self.temp_data["countdown"] = data 370 | self.temp_data["countdown"]["rtim"] = 0 371 | await self.clear_countdown_job() 372 | 373 | # 添加倒计时任务 374 | countdown_time = self.temp_data["countdown"]["ttim"] # 分钟 375 | start_date = datetime.datetime.now() 376 | end_date = start_date + datetime.timedelta(minutes=+countdown_time) 377 | self.countdown_timer = self.scheduler.add_job(self._countdown_func, "interval", minutes=1, 378 | start_date=start_date, end_date=end_date, 379 | id="countdownjob") 380 | 381 | async def _receiver(self): 382 | self.mqtt_connected.wait() 383 | 384 | logger.success("Receiver ready...") 385 | while True: 386 | data = self.received_data.get() 387 | logger.info("received msg: {0}".format(data)) 388 | 389 | if isinstance(data, str): 390 | data = json.loads(data) 391 | 392 | if "fromDevice" in data: 393 | self.target_device = data["fromDevice"] 394 | else: 395 | self.target_device = self.config.uuid 396 | 397 | if "data" in data: 398 | received_data = data["data"] 399 | else: 400 | received_data = data 401 | 402 | if "get" in received_data: 403 | if received_data["get"] == "state": 404 | if self.heartbeat_callable: 405 | await self._custom_runner(self.heartbeat_callable, msg=received_data) 406 | self.mqtt_client.send_to_device({"state": "online"}) 407 | elif received_data["get"] == "timing": 408 | self.mqtt_client.send_to_device(self.get_timing_data()) 409 | elif received_data["get"] == "countdown": 410 | self.mqtt_client.send_to_device(self.get_countdown_data()) 411 | elif "set" in received_data: 412 | if "timing" in received_data["set"]: 413 | if "dlt" in received_data["set"]["timing"][0]: 414 | await self.del_timing_data(received_data["set"]["timing"][0]["dlt"]) 415 | else: 416 | await self.set_timing_data(received_data["set"]["timing"]) 417 | 418 | self.mqtt_client.send_to_device(self.get_timing_data()) 419 | elif "countdown" in received_data["set"]: 420 | await self.set_countdown_data(received_data["set"]["countdown"]) 421 | self.mqtt_client.send_to_device(self.get_countdown_data()) 422 | elif "rt" in received_data: 423 | if self._realtime_callable: 424 | await self._custom_runner(self._realtime_callable, keys=received_data["rt"]) 425 | else: 426 | for key in received_data.keys(): 427 | if key in self.widgets.keys(): 428 | await self.widgets[key].handler(received_data) 429 | else: 430 | self.data_reader.put({"fromDevice": self.target_device, "data": {key: received_data[key]}}) 431 | 432 | await asyncio.sleep(0) 433 | 434 | async def _websocket_action(self, websocket): 435 | async for message in websocket: 436 | logger.info("websocket received msg: {0}".format(message)) 437 | self.received_data.put(message) 438 | 439 | async def init_local_service(self): 440 | self.auth_finished.wait() 441 | 442 | zero_conf = Zeroconf() 443 | # deviceType = '_' + typea 444 | # desc = {'deviceName': name} 445 | # desc = {} 446 | 447 | # info = ServiceInfo(deviceType + "._tcp.local.", 448 | # name + "." + deviceType + "._tcp.local.", 449 | # socket.inet_aton(deviceIP), 81, 0, 0, 450 | # desc, name + ".local.") 451 | info = ServiceInfo( 452 | type_="_blinker_" + self.config.deviceName[:12] + '._tcp.local.', 453 | name="_" + self.config.deviceName[:12] + '._tcp.local.', 454 | port=81, 455 | server="blinker", 456 | properties={ 457 | "mac": get_mac_address().replace(":", "").upper() 458 | } 459 | ) 460 | await zero_conf.async_register_service(info) 461 | async with websockets.serve(self._websocket_action, "localhost", 81): 462 | await asyncio.Future() 463 | 464 | # 短信通知 465 | async def sendSms(self, data: str): 466 | await self.http_client.send_sms(data[:20]) 467 | await asyncio.sleep(60) 468 | 469 | # 微信通知 470 | async def wechat(self, title: str, state: str, text: str): 471 | await self.http_client.send_wx_template_msg(title, state, text) 472 | 473 | # App通知 474 | async def push(self, data: str): 475 | await self.http_client.send_push_msg(data) 476 | 477 | # 数据存储 478 | async def saveTsData(self, data: Dict): 479 | req_data = {} 480 | for key, value in data.items(): 481 | if not isinstance(value, (int, float)): 482 | return BlinkerException(-1, "Value error") 483 | 484 | req_data[key] = [[int(time.time()), value]] 485 | await self.http_client.save_ts_data(req_data) 486 | await asyncio.sleep(60) 487 | 488 | async def saveObjectData(self, data: Dict): 489 | self.mqtt_client.send_obj_data(data) 490 | await asyncio.sleep(60) 491 | 492 | async def loadObjectData(self, keyword=None): 493 | return await self.http_client.get_object_data(keyword=keyword) 494 | 495 | async def saveTextData(self, data: str): 496 | await self.mqtt_client.send_text_data(data) 497 | await asyncio.sleep(60) 498 | 499 | async def loadTextData(self): 500 | return await self.http_client.get_text_data() 501 | 502 | async def saveLogData(self, data: str): 503 | req_data = [[int(time.time()), data]] 504 | await self.http_client.save_log_data(req_data) 505 | await asyncio.sleep(60) 506 | 507 | # 气象数据 508 | async def getAir(self, city_code=""): 509 | return await self.http_client.get_air(city_code) 510 | 511 | async def getWeather(self, city_code=""): 512 | return await self.http_client.get_weather(city_code) 513 | 514 | async def getWeatherForecast(self, city_code=""): 515 | return await self.http_client.get_forecast(city_code) 516 | 517 | # 实时数据 518 | async def sendRtData(self, key: str, data_func, t: int = 1): 519 | if key in self.realtime_tasks: 520 | self.realtime_tasks[key].remove() 521 | 522 | def rt_func(): 523 | message = {key: {"val": data_func(), "date": int(time.time())}} 524 | self.mqtt_client.send_to_device(message, to_device=self.config.uuid) 525 | 526 | self.realtime_tasks[key] = self.scheduler.add_job(rt_func, "interval", seconds=t) 527 | 528 | async def main(self): 529 | tasks = [ 530 | threading.Thread(target=asyncio.run, args=(self.device_init(),), daemon=True), 531 | threading.Thread(target=asyncio.run, args=(self.mqttclient_init(),), daemon=True), 532 | threading.Thread(target=asyncio.run, args=(self._cloud_heartbeat(),), daemon=True), 533 | threading.Thread(target=asyncio.run, args=(self._receiver(),), daemon=True), 534 | threading.Thread(target=self.scheduler_run, daemon=True) 535 | ] 536 | 537 | if self.websocket: 538 | tasks.append(threading.Thread(target=asyncio.run, args=(self.init_local_service(),))) 539 | 540 | # if self.heartbeat_callable: 541 | # tasks.append(threading.Thread(target=asyncio.run, args=(self._custom_runner(self.heartbeat_callable),))) 542 | 543 | if self.ready_callable: 544 | tasks.append(threading.Thread(target=asyncio.run, args=(self._custom_runner(self.ready_callable),))) 545 | 546 | if self.voice_assistant: 547 | tasks.append(threading.Thread(target=asyncio.run, args=(self.voice_assistant.listen(),))) 548 | 549 | # start 550 | for task in tasks: 551 | task.start() 552 | 553 | for task in tasks: 554 | task.join() 555 | 556 | def run(self): 557 | loop = asyncio.get_event_loop() 558 | try: 559 | loop.run_until_complete(self.main()) 560 | except KeyboardInterrupt as e: 561 | loop.stop() 562 | finally: 563 | loop.close() 564 | -------------------------------------------------------------------------------- /src/blinker/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | 9 | class BlinkerException(Exception): 10 | def __init__(self, message: int, detail: str = ""): 11 | self.errCode = message 12 | self.detail = detail 13 | 14 | 15 | class BlinkerHttpException(BlinkerException): 16 | pass 17 | 18 | 19 | class BlinkerBrokerException(BlinkerException): 20 | pass 21 | -------------------------------------------------------------------------------- /src/blinker/httpclient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | import json 9 | import ssl 10 | import time 11 | import certifi 12 | import aiohttp 13 | import requests 14 | 15 | from loguru import logger 16 | from typing import Dict, List 17 | from .errors import BlinkerHttpException 18 | 19 | __all__ = ["HttpClient"] 20 | 21 | ssl_context = ssl.create_default_context() 22 | ssl_context.load_verify_locations(certifi.where()) 23 | 24 | 25 | class _HttpRequestConf: 26 | SERVER = "https://iot.diandeng.tech" 27 | API = { 28 | "DIY_AUTH": SERVER + "/api/v1/user/device/diy/auth", 29 | "HEARTBEAT": SERVER + "/api/v1/user/device/heartbeat", 30 | "SMS": SERVER + '/api/v1/user/device/sms', 31 | "WECHAT": SERVER + '/api/v1/user/device/wxMsg/', 32 | "PUSH": SERVER + '/api/v1/user/device/push', 33 | "SHARE": SERVER + '/api/v1/user/device/share/device', 34 | "STORAGE_TS": SERVER + "/api/v1/user/device/cloudStorage/", 35 | "STORAGE_OBJ": SERVER + "/api/v1/user/device/cloud_storage/object", 36 | "STORAGE_TEXT": SERVER + "/api/v1/user/device/cloud_storage/text", 37 | "LOG": SERVER + '/api/v1/user/device/cloud_storage/logs', 38 | "POSITION": SERVER + '/api/v1/user/device/cloud_storage/coordinate', 39 | "WEATHER": SERVER + '/api/v3/weather', 40 | "WEATHER_FORECAST": SERVER + '/api/v3/forecast', 41 | "AIR": SERVER + '/api/v3/air', 42 | "VOICE_ASSISTANT": SERVER + "/api/v1/user/device/voice_assistant", 43 | } 44 | 45 | 46 | class HttpClient(_HttpRequestConf): 47 | def __init__(self): 48 | self.device = "" 49 | self.auth_key = "" 50 | self.auth_token = "" 51 | 52 | @staticmethod 53 | async def _async_response_handler(res: aiohttp.client.ClientResponse): 54 | if res.status != 200: 55 | raise BlinkerHttpException(-1, "Http request error, err code is {0}".format(res.status)) 56 | 57 | try: 58 | if res.content_type == "text/html": 59 | result = json.loads(await res.text()) 60 | else: 61 | result = await res.json() 62 | except Exception as e: 63 | raise BlinkerHttpException(-1, "Decode http response error, err is {0}".format(e)) 64 | 65 | if result["message"] != 1000: 66 | logger.error("code: {0}, message: {1}".format(result["message"], result["detail"])) 67 | raise BlinkerHttpException(message=result["message"], detail=result["detail"]) 68 | 69 | return result["detail"] 70 | 71 | async def _async_get(self, url: str): 72 | async with aiohttp.ClientSession() as session: 73 | async with session.get(url, ssl_context=ssl_context) as response: 74 | return await self._async_response_handler(response) 75 | 76 | async def _async_post(self, url: str, **kwargs): 77 | async with aiohttp.ClientSession() as session: 78 | async with session.post(url, **kwargs, ssl_context=ssl_context) as response: 79 | return await self._async_response_handler(response) 80 | 81 | @staticmethod 82 | def _response_handler(res): 83 | if res.status_code != 200: 84 | raise BlinkerHttpException(-1, "Http request error, err code is {0}".format(res.status)) 85 | 86 | try: 87 | result = res.json() 88 | except Exception as e: 89 | raise BlinkerHttpException(-1, "Decode http response error, err is {0}".format(e)) 90 | 91 | if result["message"] != 1000: 92 | raise BlinkerHttpException(message=result["message"], detail=result["detail"]) 93 | 94 | return result["detail"] 95 | 96 | def _get(self, url: str): 97 | res = requests.get(url) 98 | return self._response_handler(res) 99 | 100 | def _post(self, url: str, data: Dict): 101 | res = requests.post(url, data=data) 102 | return self._response_handler(res) 103 | 104 | async def diy_device_auth(self, auth_key, protocol="mqtt", version="", ali_type=None, duer_type=None, 105 | mi_type=None): 106 | url = "{0}?authKey={1}&protocol={2}".format(self.API["DIY_AUTH"], auth_key, protocol) 107 | if version: 108 | url += "&version={0}".format(version) 109 | if ali_type: 110 | url += "&aliType={0}".format(ali_type) 111 | elif duer_type: 112 | url += "&duerType={0}".format(duer_type) 113 | elif mi_type: 114 | url += "&miType={0}".format(mi_type) 115 | 116 | res = await self._async_get(url) 117 | self.auth_key = auth_key 118 | self.auth_token = res["iotToken"] 119 | self.device = res["deviceName"] 120 | 121 | return res 122 | 123 | async def get_share_info(self): 124 | url = "{0}?deviceName={1}&key={2}".format(self.API["SHARE"], self.device, self.auth_key) 125 | return await self._async_get(url) 126 | 127 | async def cloud_heartbeat(self, heartbeat=600): 128 | logger.info("http cloud heartbeat") 129 | url = "{0}?deviceName={1}&key={2}&heartbeat={3}".format(self.API["HEARTBEAT"], self.device, self.auth_key, 130 | heartbeat) 131 | return await self._async_get(url) 132 | 133 | async def set_position(self, lng, lat): 134 | url = self.API["POSITION"] 135 | data = { 136 | "token": self.auth_token, 137 | "data": [int(time.time()), [lng, lat]] 138 | } 139 | 140 | return await self._async_post(url, json=data) 141 | 142 | async def save_ts_data(self, data: Dict): 143 | """ 144 | data: {"key": [[t1, v], [t2, v], ...]} 145 | """ 146 | url = self.API["STORAGE_TS"] 147 | req_data = { 148 | "deviceName": self.device, 149 | "key": self.auth_key, 150 | "data": json.dumps(data) 151 | } 152 | 153 | return await self._async_post(url, data=req_data) 154 | 155 | async def save_log_data(self, data: List): 156 | """ 157 | data: [[t1, string], [t2, string], ...] 158 | """ 159 | 160 | url = self.API["LOG"] 161 | req_data = { 162 | "token": self.auth_token, 163 | "data": data 164 | } 165 | 166 | return await self._async_post(url, json=req_data) 167 | 168 | async def get_object_data(self, keyword=None): 169 | url = self.API["STORAGE_OBJ"] + "?deviceId=" + self.device 170 | if keyword: 171 | url += "&keyword={0}".format(keyword) 172 | 173 | return await self._async_get(url) 174 | 175 | async def get_text_data(self): 176 | url = self.API["STORAGE_TEXT"] + "?deviceId=" + self.device 177 | return await self._async_get(url) 178 | 179 | # 存储 180 | # def save_ts_data(self, data: Dict): 181 | # """ 182 | # data: {"key": [[t1, v], [t2, v], ...]} 183 | # """ 184 | # url = self.API["TS_STORAGE"] 185 | # req_data = { 186 | # "deviceName": self.device, 187 | # "key": self.auth_key, 188 | # "data": json.dumps(data) 189 | # } 190 | # 191 | # return self._post(url, req_data) 192 | 193 | # def save_log_data(self, data: List): 194 | # """ 195 | # data: [[t1, string], [t2, string], ...] 196 | # """ 197 | # 198 | # url = self.API["LOG"] 199 | # req_data = { 200 | # "token": self.auth_token, 201 | # "data": data 202 | # } 203 | # 204 | # return self._response_handler(requests.post(url, json=req_data)) 205 | 206 | # 天气 207 | async def get_weather(self, city_code): 208 | url = '{0}?device={1}&key={2}&code={3}'.format(self.API["WEATHER"], self.device, self.auth_token, city_code) 209 | return await self._async_get(url) 210 | 211 | async def get_forecast(self, city_code): 212 | url = '{0}?device={1}&key={2}&code={3}'.format(self.API["WEATHER_FORECAST"], self.device, self.auth_token, 213 | city_code) 214 | return await self._async_get(url) 215 | 216 | async def get_air(self, city_code): 217 | url = '{0}?device={1}&key={2}&code={3}'.format(self.API["AIR"], self.device, self.auth_token, city_code) 218 | return await self._async_get(url) 219 | 220 | # 消息 221 | async def send_sms(self, data: str, phone: str = ""): 222 | url = self.API["SMS"] 223 | req_data = { 224 | "deviceName": self.device, 225 | "key": self.auth_key, 226 | "cel": phone, 227 | "msg": data 228 | } 229 | 230 | return await self._async_post(url, data=req_data) 231 | 232 | async def send_wx_template_msg(self, title, state, text): 233 | url = self.API["WECHAT"] 234 | req_data = { 235 | "deviceName": self.device, 236 | "key": self.auth_key, 237 | "title": title, 238 | "state": state, 239 | "msg": text 240 | } 241 | 242 | return await self._async_post(url, data=req_data) 243 | 244 | async def send_push_msg(self, msg: str, receivers: List = None): 245 | url = self.API["PUSH"] 246 | req_data = { 247 | "deviceName": self.device, 248 | "key": self.auth_key, 249 | "msg": msg 250 | } 251 | 252 | if receivers: 253 | req_data["receivers"] = receivers 254 | 255 | return await self._async_post(url, data=req_data) 256 | -------------------------------------------------------------------------------- /src/blinker/mqttclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "stao" 5 | 6 | import json 7 | import ssl 8 | import certifi 9 | 10 | from typing import Dict, Any, List 11 | from loguru import logger 12 | from queue import SimpleQueue 13 | from paho.mqtt.client import Client 14 | from .errors import BlinkerBrokerException 15 | 16 | __all__ = ["MqttClient"] 17 | 18 | ssl_context = ssl.create_default_context() 19 | ssl_context.load_verify_locations(certifi.where()) 20 | 21 | 22 | class MqttClient: 23 | device = None 24 | 25 | def __init__(self, device): 26 | self.device = device 27 | self.name = device.config.broker 28 | self.client_id = device.config.deviceName 29 | self.client = Client(client_id=self.client_id) 30 | self._sub_topic = f"/device/{self.client_id}/r" 31 | self._pub_topic = f"/device/{self.client_id}/s" 32 | self._exasub_topic = f"/device/ServerSender/r" 33 | self._exapub_topic = f"/device/ServerReceiver/s" 34 | 35 | self.port = device.config.port 36 | self.username = device.config.iotId 37 | self.password = device.config.iotToken 38 | 39 | mqtt_url = device.config.host.split("//") 40 | self.protocol = mqtt_url[0] 41 | self.host = mqtt_url[-1] 42 | 43 | self.device.mqtt_client = self 44 | 45 | def _on_connect(self, client, userdata, flags, rc): 46 | logger.info("Connect to broker from mqtt") 47 | if rc == 0: 48 | logger.success("Broker connected...") 49 | self.device.mqtt_connected.set() 50 | self.client.subscribe([(self._sub_topic, 0), (self._exasub_topic, 0)]) 51 | else: 52 | logger.error("Connect to broker error, code is {0}".format(rc)) 53 | 54 | def _on_message(self, client, userdata, msg): 55 | received_msg = msg.payload.decode("utf8") 56 | 57 | if isinstance(received_msg, str): 58 | try: 59 | received_data = json.loads(received_msg) 60 | except json.decoder.JSONDecodeError: 61 | received_data = None 62 | logger.error("mqtt received msg isn't json") 63 | else: 64 | received_data = received_msg 65 | 66 | if received_data: 67 | if received_data["fromDevice"] == "ServerSender": 68 | self.device.voice_assistant.va_received_data.put(received_data) 69 | else: 70 | self.device.received_data.put(received_data) 71 | 72 | async def connection(self): 73 | self.client.username_pw_set(self.username, self.password) 74 | self.client.on_connect = self._on_connect 75 | self.client.on_message = self._on_message 76 | 77 | if self.protocol == "mqtts:": 78 | self.client.tls_set_context(context=ssl_context) 79 | self.client.connect_async(self.host, int(self.port)) 80 | self.client.loop_start() 81 | 82 | def _get_target_device(self, to_device: str = None): 83 | if to_device: 84 | return to_device 85 | if self.device.target_device: 86 | return self.device.target_device 87 | return self.device.config.uuid 88 | 89 | def _format_msg_to_device(self, data: Any, to_device: str = "") -> Dict: 90 | return {"deviceType": "OwnApp", "fromDevice": self.client_id, "toDevice": self._get_target_device(to_device), 91 | "data": data} 92 | 93 | def _format_msg_to_group(self, data: Any, to_group: str) -> Dict: 94 | return {"fromDevice": self.client_id, "toGroup": to_group, "data": data} 95 | 96 | def _format_msg_to_storage(self, data: Any, storage_type: str) -> Dict: 97 | return {"fromDevice": self.client_id, "toStorage": storage_type, "data": data} 98 | 99 | def _format_msg_to_voiceassistant(self, data: Any) -> Dict: 100 | return {"fromDevice": self.client_id, "toDevice": "ServerReceiver", "data": data} 101 | 102 | def _check_or_reconnect(self): 103 | if not self.client.is_connected(): 104 | self.client.reconnect() 105 | 106 | def send_to_device(self, data, to_device: str = None): 107 | self._check_or_reconnect() 108 | payload = json.dumps(self._format_msg_to_device(data, to_device)) 109 | logger.info("send mqtt message: {0}".format(payload)) 110 | self.client.publish(self._pub_topic, payload) 111 | 112 | def send_to_voiceassistant(self, data): 113 | self._check_or_reconnect() 114 | payload = json.dumps(self._format_msg_to_voiceassistant(data)) 115 | logger.info("send mqtt message to voice assistant: {0}".format(payload)) 116 | self.client.publish(self._exapub_topic, payload) 117 | 118 | # 存储 119 | def _save_check(self): 120 | if self.name != "blinker": 121 | raise BlinkerBrokerException(-1, "仅可用于blinker broker") 122 | 123 | def send_ts_data(self, data: List): 124 | self._check_or_reconnect() 125 | payload = json.dumps(self._format_msg_to_storage(data, "ts")) 126 | payload = payload.replace(" ", "") 127 | self.client.publish(self._pub_topic, payload) 128 | logger.info("sended ts data") 129 | 130 | def send_obj_data(self, data: Dict): 131 | self._check_or_reconnect() 132 | # data = json.dumps(data) 133 | payload = json.dumps(self._format_msg_to_storage(data, "ot")) 134 | payload = payload.replace(" ", "") 135 | logger.info("mqtt message topic: {0}".format(self._pub_topic)) 136 | logger.info("mqtt message payload: {0}".format(payload)) 137 | self.client.publish(self._pub_topic, payload) 138 | 139 | def send_text_data(self, data: str): 140 | self._check_or_reconnect() 141 | payload = json.dumps(self._format_msg_to_storage(data, "tt")) 142 | payload = payload.replace(" ", "") 143 | self.client.publish(self._pub_topic, payload) 144 | -------------------------------------------------------------------------------- /src/blinker/voice_assistant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from asyncio.coroutines import iscoroutinefunction 9 | from queue import SimpleQueue 10 | from loguru import logger 11 | from typing import Dict, Union, List 12 | 13 | 14 | class VAType: 15 | LIGHT = "light" 16 | OUTLET = "outlet" 17 | MULTI_OUTLET = "multi_outlet" 18 | SENSOR = "sensor" 19 | FAN = "fan" 20 | AIRCONDITION = "aircondition" 21 | 22 | 23 | class MiLightMode: 24 | DAY = "day" 25 | NIGHT = "night" 26 | COLOR = "color" 27 | WARMTH = "warmth" 28 | TV = "tv" 29 | READING = "reading" 30 | COMPUTER = "computer" 31 | 32 | 33 | class AliLightMode: 34 | READING = "reading" 35 | MOVIE = 'movie' 36 | SLEEP = 'sleep' 37 | LIVE = 'live' 38 | HOLIDAY = 'holiday' 39 | MUSIC = 'music' 40 | COMMON = 'common' 41 | NIGHT = 'night' 42 | 43 | 44 | class DuerLightMode: 45 | READING = 'READING' 46 | SLEEP = 'SLEEP' 47 | ALARM = 'ALARM' 48 | NIGHT_LIGHT = 'NIGHT_LIGHT' 49 | ROMANTIC = 'ROMANTIC' 50 | SUNDOWN = 'SUNDOWN' 51 | SUNRISE = 'SUNRISE' 52 | RELAX = 'RELAX' 53 | LIGHTING = 'LIGHTING' 54 | SUN = 'SUN' 55 | STAR = 'STAR' 56 | ENERGY_SAVING = 'ENERGY_SAVING' 57 | MOON = 'MOON' 58 | JUDI = 'JUDI' 59 | 60 | 61 | class VAMessage(object): 62 | voice_assistant = None 63 | 64 | def __init__(self, voice_assistant, data: Dict = None): 65 | self.voice_assistant = voice_assistant 66 | self.data = data 67 | self.push_data = {} 68 | 69 | def send(self): 70 | raise NotImplemented 71 | 72 | async def update(self): 73 | await self.voice_assistant.push_msg(self.push_data) 74 | 75 | 76 | class PowerMessage(VAMessage): 77 | async def power(self, state): 78 | self.push_data["pState"] = state 79 | return self 80 | 81 | async def num(self, num: Union[int, float]): 82 | self.push_data["num"] = num 83 | return self 84 | 85 | 86 | class ModeMessage(VAMessage): 87 | async def mode(self, state: Union[str, int, float]): 88 | self.push_data["mode"] = state 89 | return self 90 | 91 | 92 | class ColorMessage(VAMessage): 93 | async def color(self, color: Union[str, int, float]): 94 | self.push_data["clr"] = color 95 | return self 96 | 97 | 98 | class ColorTempMessage(VAMessage): 99 | async def colorTemp(self, val: Union[int, float]): 100 | self.push_data["colTemp"] = val 101 | return self 102 | 103 | 104 | class BrightnessMessage(VAMessage): 105 | async def brightness(self, val: Union[int, float, str]): 106 | self.push_data["bright"] = val 107 | return self 108 | 109 | 110 | class DataMessage(VAMessage): 111 | async def temp(self, val: Union[int, float, str]): 112 | self.push_data["temp"] = str(val) 113 | return self 114 | 115 | async def humi(self, val: Union[int, float, str]): 116 | self.push_data["humi"] = str(val) 117 | return self 118 | 119 | async def aqi(self, val: Union[int, float, str]): 120 | self.push_data["aqi"] = str(val) 121 | return self 122 | 123 | async def pm25(self, val: Union[int, float, str]): 124 | self.push_data["pm25"] = str(val) 125 | return self 126 | 127 | async def pm10(self, val: Union[int, float, str]): 128 | self.push_data["pm10"] = str(val) 129 | return self 130 | 131 | async def co2(self, val: Union[int, float, str]): 132 | self.push_data["co2"] = str(val) 133 | return self 134 | 135 | async def brightness(self, val: Union[int, float, str]): 136 | self.push_data["brightness"] = str(val) 137 | return self 138 | 139 | async def color(self, val: Union[int, float, str, List[int]]): 140 | self.push_data["color"] = val 141 | return self 142 | 143 | async def colorTemp(self, val: Union[int, float, str]): 144 | self.push_data["colorTemp"] = str(val) 145 | return self 146 | 147 | async def mode(self, state: Union[int, float, str]): 148 | self.push_data["mode"] = state 149 | return self 150 | 151 | async def power(self, state: str): 152 | if self.voice_assistant.va_name == "MIOT": 153 | if state == "on": 154 | state = "true" 155 | else: 156 | state = "false" 157 | 158 | self.push_data["pState"] = state 159 | return self 160 | 161 | 162 | class VoiceAssistant(object): 163 | device = None 164 | va_received_data = SimpleQueue() 165 | 166 | def __init__(self, va_type, power_change=None, mode_change=None, color_change=None, colortemp_change=None, 167 | brightness_change=None, state_query=None): 168 | self.va_type = va_type 169 | 170 | self.va_name = "" 171 | self.message_id = "" 172 | 173 | self._power_change_callable = power_change 174 | self._mode_change_callable = mode_change 175 | self._color_change_callable = color_change 176 | self._colortemp_change_callable = colortemp_change 177 | self._brightness_change_callable = brightness_change 178 | self._state_query_callable = state_query 179 | 180 | @property 181 | def power_change_callable(self): 182 | return self._power_change_callable 183 | 184 | @power_change_callable.setter 185 | def power_change_callable(self, func): 186 | self._power_change_callable = func 187 | 188 | @property 189 | def mode_change_callable(self): 190 | return self._mode_change_callable 191 | 192 | @mode_change_callable.setter 193 | def mode_change_callable(self, func): 194 | self._mode_change_callable = func 195 | 196 | @property 197 | def color_change_callable(self): 198 | return self._color_change_callable 199 | 200 | @color_change_callable.setter 201 | def color_change_callable(self, func): 202 | self._color_change_callable = func 203 | 204 | @property 205 | def colortemp_change_callable(self): 206 | return self._colortemp_change_callable 207 | 208 | @colortemp_change_callable.setter 209 | def colortemp_change_callable(self, func): 210 | self._colortemp_change_callable = func 211 | 212 | @property 213 | def brightness_change_callable(self): 214 | return self._brightness_change_callable 215 | 216 | @brightness_change_callable.setter 217 | def brightness_change_callable(self, func): 218 | self._brightness_change_callable = func 219 | 220 | @property 221 | def state_query_callable(self): 222 | return self._state_query_callable 223 | 224 | @state_query_callable.setter 225 | def state_query_callable(self, func): 226 | self._state_query_callable = func 227 | 228 | @staticmethod 229 | async def custom_run(func, data): 230 | if iscoroutinefunction(func): 231 | await func(data) 232 | else: 233 | func(data) 234 | 235 | async def push_msg(self, data: Dict): 236 | data["messageId"] = self.message_id 237 | self.device.mqtt_client.send_to_voiceassistant(data) 238 | 239 | async def listen(self): 240 | while True: 241 | received_msg = self.va_received_data.get() 242 | # self.target_device = received_msg["fromDevice"] 243 | self.va_name = received_msg["data"]["from"] 244 | self.message_id = received_msg["data"]["messageId"] 245 | 246 | await self.process_data(received_msg["data"]) 247 | 248 | async def process_data(self, data): 249 | if "get" in data: 250 | await self.custom_run(self.state_query_callable, DataMessage(self, data["get"])) 251 | elif "set" in data: 252 | set_ops = data["set"] 253 | if "pState" in set_ops: 254 | await self.custom_run(self.power_change_callable, PowerMessage(self, set_ops)) 255 | elif "col" in set_ops: 256 | await self.custom_run(self.color_change_callable, ColorMessage(self, set_ops)) 257 | elif "colTemp" in set_ops: 258 | await self.custom_run(self.colortemp_change_callable, ColorTempMessage(self, set_ops)) 259 | elif "mode" in set_ops: 260 | await self.custom_run(self.mode_change_callable, ModeMessage(self, set_ops)) 261 | elif "bright" in set_ops or "downBright" in set_ops or "upBright" in set_ops: 262 | await self.custom_run(self.brightness_change_callable, BrightnessMessage(self, set_ops)) 263 | else: 264 | logger.warning("Not support set operate: {0}".format(set_ops)) 265 | else: 266 | logger.warning("Not support voice assistant ops: {0}".format(data)) 267 | -------------------------------------------------------------------------------- /src/blinker/widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | __author__ = 'stao' 7 | 8 | from asyncio.coroutines import iscoroutinefunction 9 | from loguru import logger 10 | 11 | __all__ = ["ButtonWidget", "TextWidget", "NumberWidget", "RangeWidget", "RGBWidget", "JoystickWidget", 12 | "ImageWidget", "VideoWidget", "ChartWidget"] 13 | 14 | 15 | class _Widget: 16 | device = None 17 | 18 | def __init__(self, key: str, func=None): 19 | self.key = key 20 | self.state = {} 21 | 22 | self._device = None 23 | self.targetDevice = "" 24 | 25 | self._func = func 26 | 27 | @property 28 | def func(self): 29 | return self._func 30 | 31 | @func.setter 32 | def func(self, func): 33 | self._func = func 34 | 35 | async def handler(self, msg): 36 | if self.func: 37 | if iscoroutinefunction(self._func): 38 | await self._func(msg) 39 | else: 40 | self._func(msg) 41 | else: 42 | logger.warning("Not setting callable func for {0}".format(self.key)) 43 | 44 | def set_state(self, state): 45 | self.state = state 46 | return self 47 | 48 | async def update(self): 49 | message = {self.key: self.state} 50 | self.device.mqtt_client.send_to_device(message) 51 | 52 | # def listen(self): 53 | # self._change.subscribe(lambda msg: self._sub_change_func(msg)) 54 | # return self.change 55 | 56 | def _sub_change_func(self, msg): 57 | # self.device.target_device = msg["fromDevice"] 58 | # return self.change.on_next(msg["data"]) 59 | try: 60 | self._func(msg) 61 | except TypeError: 62 | logger.error("Widget {0} not set callback func".format(self.key)) 63 | 64 | 65 | class ButtonWidget(_Widget): 66 | def turn(self, swi): 67 | self.state['swi'] = swi 68 | return self 69 | 70 | def text(self, text): 71 | self.state["tex"] = text 72 | return self 73 | 74 | def icon(self, icon): 75 | self.state["ico"] = icon 76 | return self 77 | 78 | def color(self, color): 79 | self.state["clr"] = color 80 | return self 81 | 82 | 83 | class TextWidget(_Widget): 84 | def text(self, text): 85 | self.state["tex"] = text 86 | return self 87 | 88 | def text1(self, text): 89 | self.state["tex1"] = text 90 | return self 91 | 92 | def icon(self, icon): 93 | self.state["ico"] = icon 94 | return self 95 | 96 | def color(self, color): 97 | self.state["clr"] = color 98 | return self 99 | 100 | 101 | class NumberWidget(_Widget): 102 | def text(self, text): 103 | self.state["tex"] = text 104 | return self 105 | 106 | def value(self, value): 107 | self.state["val"] = value 108 | return self 109 | 110 | def unit(self, unit): 111 | self.state["uni"] = unit 112 | return self 113 | 114 | def icon(self, icon): 115 | self.state["ico"] = icon 116 | return self 117 | 118 | def color(self, color): 119 | self.state["clr"] = color 120 | return self 121 | 122 | def max(self, num): 123 | self.state["max"] = num 124 | return self 125 | 126 | 127 | class RangeWidget(_Widget): 128 | def text(self, text): 129 | self.state["tex"] = text 130 | return self 131 | 132 | def value(self, value): 133 | self.state["val"] = value 134 | return self 135 | 136 | def unit(self, unit): 137 | self.state["uni"] = unit 138 | return self 139 | 140 | def icon(self, icon): 141 | self.state["ico"] = icon 142 | return self 143 | 144 | def color(self, color): 145 | self.state["clr"] = color 146 | return self 147 | 148 | def max(self, num): 149 | self.state["max"] = num 150 | return self 151 | 152 | 153 | class RGBWidget(_Widget): 154 | def text(self, text): 155 | self.state["tex"] = text 156 | return self 157 | 158 | def color(self, color): 159 | if type(color) == str and color[0] != "#": 160 | self.state = self.to_rgb(color) 161 | elif len(color) in (3, 4): 162 | self.state = color 163 | return self 164 | 165 | def brightness(self, brightness): 166 | self.state[3] = brightness 167 | return self 168 | 169 | @staticmethod 170 | def to_rgb(color_hex): 171 | r = str(int(color_hex[1:3], 16)) 172 | g = str(int(color_hex[3:5], 16)) 173 | b = str(int(color_hex[5:7], 16)) 174 | return [r, g, b] 175 | 176 | 177 | class JoystickWidget(_Widget): 178 | pass 179 | 180 | 181 | class ImageWidget(_Widget): 182 | def show(self, img): 183 | self.state["img"] = img 184 | return self 185 | 186 | 187 | class VideoWidget(_Widget): 188 | def url(self, addr: str): 189 | self.state["url"] = addr 190 | return self 191 | 192 | def autoplay(self, swi: bool): 193 | self.state["auto"] = swi 194 | return self 195 | 196 | 197 | class ChartWidget(_Widget): 198 | pass 199 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"version": "0.3.0"} --------------------------------------------------------------------------------