├── .github └── workflows │ └── projects.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── config.py ├── connect.py ├── example ├── esp32_wrover_kit_rgb.py ├── multiple_things.py ├── single_thing.py └── sparkfun_esp32_thing.py ├── start.py ├── upy ├── README.md ├── copy.py ├── eventemitter.py ├── logging.py ├── types.py └── uuid.py └── webthing ├── action.py ├── errors.py ├── event.py ├── property.py ├── server.py ├── thing.py ├── utils.py └── value.py /.github/workflows/projects.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to the specified project column 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | add-new-issues-to-project-column: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: add-new-issues-to-organization-based-project-column 12 | uses: docker://takanabe/github-actions-automate-projects:v0.0.1 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.CI_TOKEN }} 15 | GITHUB_PROJECT_URL: https://github.com/orgs/WebThingsIO/projects/4 16 | GITHUB_PROJECT_COLUMN_NAME: To do 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mozilla IoT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webthing-upy 2 | 3 | This is a MicroPython version of webthing-python. 4 | 5 | This has been tested on an ESP-WROVER-KIT and a SparkFun ESP32 Thing using the 6 | loboris version of ESP32 MicroPython. 7 | The loboris port has a forked copy of https://github.com/jczic/MicroWebSrv and 8 | this requires some further changes which can be found here: 9 | 10 | https://github.com/dhylands/MicroPython_ESP32_psRAM_LoBo/tree/rest-improvements 11 | 12 | # Building and Flashing MicroPython 13 | 14 | Using https://github.com/dhylands/MicroPython_ESP32_psRAM_LoBo/tree/rest-improvements follow 15 | the directions in the [README.md](https://github.com/dhylands/MicroPython_ESP32_psRAM_LoBo/tree/rest-improvements/README.md) file 16 | 17 | # Installing webthing-upy 18 | 19 | I used version 0.0.12 of [rshell](https://github.com/dhylands/rshell) to copy the webthing-upy files to the board. The 20 | ESP-WROVER-KIT board advertises 2 serial ports. Use the second port (typically /dev/ttyUSB1). The SparkFun ESP32 Thing only advertises a single serial port. 21 | 22 | Edit the config.py with an appropriate SSID and password. Edit main.py to be appropriate for the board you're using. 23 | 24 | Sample main.py for the SparkFun ESP32 Thing: 25 | ``` 26 | import start 27 | start.thing() 28 | ``` 29 | Sample main.py for the ESP-WROVER-KIT: 30 | ``` 31 | import start 32 | start.rgb() 33 | ``` 34 | For debugging, remove main.py and enter commands at the REPL manually. 35 | 36 | ``` 37 | $ cd webthing-upy 38 | $ rshell -a --buffer-size=30 --port=/dev/ttyUSB1 39 | webthing-upy> rsync -v . /flash 40 | webthing-upy> repl 41 | >>> Control-D 42 | ``` 43 | Pressing Control-D will cause the board to soft reboot which will start executing main.py. 44 | 45 | # Adding to Gateway 46 | 47 | To add your web thing to the WebThings Gateway, install the "Web Thing" add-on and follow the instructions [here](https://github.com/WebThingsIO/thing-url-adapter#readme). 48 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # These should be edited to reflect your actual SSID and password 2 | 3 | SSID = '' 4 | PASSWORD = '' 5 | 6 | if SSID == '': 7 | print('Please edit config.py and set the SSID and password') 8 | raise ValueError('SSID not set') 9 | -------------------------------------------------------------------------------- /connect.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import network 3 | import time 4 | import config 5 | 6 | 7 | def start_ftp(): 8 | print('Starting FTP...') 9 | network.ftp.start() 10 | 11 | 12 | def start_ntp(): 13 | print('Syncing to NTP...') 14 | rtc = machine.RTC() 15 | rtc.ntp_sync(server='pool.ntp.org') 16 | 17 | if not rtc.synced(): 18 | print(' waiting for time sync...', end='') 19 | time.sleep(0.5) 20 | while not rtc.synced(): 21 | print('.', end='') 22 | time.sleep(0.5) 23 | print('') 24 | print('Time:', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())) 25 | 26 | 27 | def connect_to_ap(): 28 | station = network.WLAN(network.STA_IF) 29 | if not station.active(): 30 | station.active(True) 31 | if not station.isconnected(): 32 | print('Connecting....') 33 | station.connect(config.SSID, config.PASSWORD) 34 | while not station.isconnected(): 35 | time.sleep(1) 36 | print('.', end='') 37 | print('') 38 | print('ifconfig =', station.ifconfig()) 39 | -------------------------------------------------------------------------------- /example/esp32_wrover_kit_rgb.py: -------------------------------------------------------------------------------- 1 | from property import Property 2 | from thing import Thing 3 | from value import Value 4 | from server import SingleThing, WebThingServer 5 | import logging 6 | import machine 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class RGBLed(Thing): 12 | 13 | def __init__(self, rPin, gPin, bPin): 14 | Thing.__init__( 15 | self, 16 | 'urn:dev:ops:esp32-rgb-led-1234', 17 | 'ESP32-RGB-LED', 18 | ['OnOffSwitch', 'Light', 'ColorControl'], 19 | 'RGB LED on ESP-Wrover-Kit' 20 | ) 21 | self.pinRed = machine.Pin(rPin, machine.Pin.OUT) 22 | self.pinGreen = machine.Pin(gPin, machine.Pin.OUT) 23 | self.pinBlue = machine.Pin(bPin, machine.Pin.OUT) 24 | self.pwmRed = machine.PWM(self.pinRed) 25 | self.pwmGreen = machine.PWM(self.pinGreen) 26 | self.pwmBlue = machine.PWM(self.pinBlue) 27 | self.redLevel = 50 28 | self.greenLevel = 50 29 | self.blueLevel = 50 30 | self.on = False 31 | self.updateLeds() 32 | 33 | self.add_property( 34 | Property(self, 35 | 'on', 36 | Value(True, self.setOnOff), 37 | metadata={ 38 | '@type': 'OnOffProperty', 39 | 'title': 'On/Off', 40 | 'type': 'boolean', 41 | 'description': 'Whether the LED is turned on', 42 | })) 43 | self.add_property( 44 | Property(self, 45 | 'color', 46 | Value('#808080', self.setRGBColor), 47 | metadata={ 48 | '@type': 'ColorProperty', 49 | 'title': 'Color', 50 | 'type': 'string', 51 | 'description': 'The color of the LED', 52 | })) 53 | 54 | def setOnOff(self, onOff): 55 | print('setOnOff: onOff =', onOff) 56 | self.on = onOff 57 | self.updateLeds() 58 | 59 | def setRGBColor(self, color): 60 | print('setRGBColor: color =', color) 61 | self.redLevel = int(color[1:3], 16) / 256 * 100 62 | self.greenLevel = int(color[3:5], 16) / 256 * 100 63 | self.blueLevel = int(color[5:7], 16) / 256 * 100 64 | self.updateLeds() 65 | 66 | def updateLeds(self): 67 | print('updateLeds: on =', self.on, 'r', self.redLevel, 68 | 'g', self.greenLevel, 'b', self.blueLevel) 69 | if self.on: 70 | self.pwmRed.duty(self.redLevel) 71 | self.pwmGreen.duty(self.greenLevel) 72 | self.pwmBlue.duty(self.blueLevel) 73 | else: 74 | self.pwmRed.duty(0) 75 | self.pwmGreen.duty(0) 76 | self.pwmBlue.duty(0) 77 | 78 | 79 | def run_server(): 80 | log.info('run_server') 81 | 82 | rgb = RGBLed(0, 2, 4) 83 | 84 | # If adding more than one thing here, be sure to set the `name` 85 | # parameter to some string, which will be broadcast via mDNS. 86 | # In the single thing case, the thing's name will be broadcast. 87 | server = WebThingServer(SingleThing(rgb), port=80) 88 | try: 89 | log.info('starting the server') 90 | server.start() 91 | except KeyboardInterrupt: 92 | log.info('stopping the server') 93 | server.stop() 94 | log.info('done') 95 | -------------------------------------------------------------------------------- /example/multiple_things.py: -------------------------------------------------------------------------------- 1 | from action import Action 2 | from event import Event 3 | from property import Property 4 | from thing import Thing 5 | from value import Value 6 | from server import MultipleThings, WebThingServer 7 | import logging 8 | import random 9 | import time 10 | import uuid 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class OverheatedEvent(Event): 16 | 17 | def __init__(self, thing, data): 18 | Event.__init__(self, thing, 'overheated', data=data) 19 | 20 | 21 | class FadeAction(Action): 22 | 23 | def __init__(self, thing, input_): 24 | Action.__init__(self, uuid.uuid4().hex, thing, 'fade', input_=input_) 25 | 26 | def perform_action(self): 27 | time.sleep(self.input['duration'] / 1000) 28 | self.thing.set_property('brightness', self.input['brightness']) 29 | self.thing.add_event(OverheatedEvent(self.thing, 102)) 30 | 31 | 32 | class ExampleDimmableLight(Thing): 33 | """A dimmable light that logs received commands to stdout.""" 34 | 35 | def __init__(self): 36 | Thing.__init__( 37 | self, 38 | 'urn:dev:ops:my-lamp-1234', 39 | 'My Lamp', 40 | ['OnOffSwitch', 'Light'], 41 | 'A web connected lamp' 42 | ) 43 | 44 | self.add_property( 45 | Property(self, 46 | 'on', 47 | Value(True, lambda v: print('On-State is now', v)), 48 | metadata={ 49 | '@type': 'OnOffProperty', 50 | 'title': 'On/Off', 51 | 'type': 'boolean', 52 | 'description': 'Whether the lamp is turned on', 53 | })) 54 | 55 | self.add_property( 56 | Property(self, 57 | 'brightness', 58 | Value(50, lambda v: print('Brightness is now', v)), 59 | metadata={ 60 | '@type': 'BrightnessProperty', 61 | 'title': 'Brightness', 62 | 'type': 'integer', 63 | 'description': 'The level of light from 0-100', 64 | 'minimum': 0, 65 | 'maximum': 100, 66 | 'unit': 'percent', 67 | })) 68 | 69 | self.add_available_action( 70 | 'fade', 71 | { 72 | 'title': 'Fade', 73 | 'description': 'Fade the lamp to a given level', 74 | 'input': { 75 | 'type': 'object', 76 | 'required': [ 77 | 'brightness', 78 | 'duration', 79 | ], 80 | 'properties': { 81 | 'brightness': { 82 | 'type': 'integer', 83 | 'minimum': 0, 84 | 'maximum': 100, 85 | 'unit': 'percent', 86 | }, 87 | 'duration': { 88 | 'type': 'integer', 89 | 'minimum': 1, 90 | 'unit': 'milliseconds', 91 | }, 92 | }, 93 | }, 94 | }, 95 | FadeAction) 96 | 97 | self.add_available_event( 98 | 'overheated', 99 | { 100 | 'description': 101 | 'The lamp has exceeded its safe operating temperature', 102 | 'type': 'number', 103 | 'unit': 'degree celsius', 104 | }) 105 | 106 | 107 | class FakeGpioHumiditySensor(Thing): 108 | """A humidity sensor which updates its measurement every few seconds.""" 109 | 110 | def __init__(self): 111 | Thing.__init__( 112 | self, 113 | 'urn:dev:ops:my-humidity-sensor-1234', 114 | 'My Humidity Sensor', 115 | ['MultiLevelSensor'], 116 | 'A web connected humidity sensor' 117 | ) 118 | 119 | self.level = Value(0.0) 120 | self.add_property( 121 | Property(self, 122 | 'level', 123 | self.level, 124 | metadata={ 125 | '@type': 'LevelProperty', 126 | 'title': 'Humidity', 127 | 'type': 'number', 128 | 'description': 'The current humidity in %', 129 | 'minimum': 0, 130 | 'maximum': 100, 131 | 'unit': 'percent', 132 | 'readOnly': True, 133 | })) 134 | 135 | log.debug('starting the sensor update looping task') 136 | 137 | @staticmethod 138 | def read_from_gpio(): 139 | """Mimic an actual sensor updating its reading every couple seconds.""" 140 | return abs(70.0 * random.random() * (-0.5 + random.random())) 141 | 142 | 143 | def run_server(): 144 | log.info('run_server') 145 | 146 | # Create a thing that represents a dimmable light 147 | light = ExampleDimmableLight() 148 | 149 | # Create a thing that represents a humidity sensor 150 | sensor = FakeGpioHumiditySensor() 151 | 152 | # If adding more than one thing, use MultipleThings() with a name. 153 | # In the single thing case, the thing's name will be broadcast. 154 | server = WebThingServer(MultipleThings([light, sensor], 155 | 'LightAndTempDevice'), 156 | port=80) 157 | try: 158 | log.info('starting the server') 159 | server.start() 160 | except KeyboardInterrupt: 161 | log.info('stopping the server') 162 | server.stop() 163 | log.info('done') 164 | 165 | 166 | if __name__ == '__main__': 167 | log.basicConfig( 168 | level=10, 169 | format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s" 170 | ) 171 | run_server() 172 | -------------------------------------------------------------------------------- /example/single_thing.py: -------------------------------------------------------------------------------- 1 | from action import Action 2 | from event import Event 3 | from property import Property 4 | from thing import Thing 5 | from value import Value 6 | from server import SingleThing, WebThingServer 7 | import logging 8 | import time 9 | import uuid 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class OverheatedEvent(Event): 15 | 16 | def __init__(self, thing, data): 17 | Event.__init__(self, thing, 'overheated', data=data) 18 | 19 | 20 | class FadeAction(Action): 21 | 22 | def __init__(self, thing, input_): 23 | Action.__init__(self, uuid.uuid4().hex, thing, 'fade', input_=input_) 24 | 25 | def perform_action(self): 26 | time.sleep(self.input['duration'] / 1000) 27 | self.thing.set_property('brightness', self.input['brightness']) 28 | self.thing.add_event(OverheatedEvent(self.thing, 102)) 29 | 30 | 31 | def make_thing(): 32 | thing = Thing( 33 | 'urn:dev:ops:my-lamp-1234', 34 | 'My Lamp', 35 | ['OnOffSwitch', 'Light'], 36 | 'A web connected lamp' 37 | ) 38 | 39 | thing.add_property( 40 | Property(thing, 41 | 'on', 42 | Value(True), 43 | metadata={ 44 | '@type': 'OnOffProperty', 45 | 'title': 'On/Off', 46 | 'type': 'boolean', 47 | 'description': 'Whether the lamp is turned on', 48 | })) 49 | thing.add_property( 50 | Property(thing, 51 | 'brightness', 52 | Value(50), 53 | metadata={ 54 | '@type': 'BrightnessProperty', 55 | 'title': 'Brightness', 56 | 'type': 'integer', 57 | 'description': 'The level of light from 0-100', 58 | 'minimum': 0, 59 | 'maximum': 100, 60 | 'unit': 'percent', 61 | })) 62 | 63 | thing.add_available_action( 64 | 'fade', 65 | { 66 | 'title': 'Fade', 67 | 'description': 'Fade the lamp to a given level', 68 | 'input': { 69 | 'type': 'object', 70 | 'required': [ 71 | 'brightness', 72 | 'duration', 73 | ], 74 | 'properties': { 75 | 'brightness': { 76 | 'type': 'integer', 77 | 'minimum': 0, 78 | 'maximum': 100, 79 | 'unit': 'percent', 80 | }, 81 | 'duration': { 82 | 'type': 'integer', 83 | 'minimum': 1, 84 | 'unit': 'milliseconds', 85 | }, 86 | }, 87 | }, 88 | }, 89 | FadeAction) 90 | 91 | thing.add_available_event( 92 | 'overheated', 93 | { 94 | 'description': 95 | 'The lamp has exceeded its safe operating temperature', 96 | 'type': 'number', 97 | 'unit': 'degree celsius', 98 | }) 99 | 100 | return thing 101 | 102 | 103 | def run_server(): 104 | log.info('run_server') 105 | 106 | thing = make_thing() 107 | 108 | # If adding more than one thing, use MultipleThings() with a name. 109 | # In the single thing case, the thing's name will be broadcast. 110 | server = WebThingServer(SingleThing(thing), port=80) 111 | try: 112 | log.info('starting the server') 113 | server.start() 114 | except KeyboardInterrupt: 115 | log.info('stopping the server') 116 | server.stop() 117 | log.info('done') 118 | 119 | 120 | if __name__ == '__main__': 121 | log.basicConfig( 122 | level=10, 123 | format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s" 124 | ) 125 | run_server() 126 | -------------------------------------------------------------------------------- /example/sparkfun_esp32_thing.py: -------------------------------------------------------------------------------- 1 | from property import Property 2 | from thing import Thing 3 | from value import Value 4 | from server import MultipleThings, WebThingServer 5 | import logging 6 | import time 7 | import machine 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Led(Thing): 13 | 14 | def __init__(self, ledPin): 15 | Thing.__init__( 16 | self, 17 | 'urn:dev:ops:blue-led-1234', 18 | 'Blue LED', 19 | ['OnOffSwitch', 'Light'], 20 | 'Blue LED on SparkFun ESP32 Thing' 21 | ) 22 | self.pinLed = machine.Pin(ledPin, machine.Pin.OUT) 23 | self.pwmLed = machine.PWM(self.pinLed) 24 | self.ledBrightness = 50 25 | self.on = False 26 | self.updateLed() 27 | 28 | self.add_property( 29 | Property(self, 30 | 'on', 31 | Value(self.on, self.setOnOff), 32 | metadata={ 33 | '@type': 'OnOffProperty', 34 | 'title': 'On/Off', 35 | 'type': 'boolean', 36 | 'description': 'Whether the LED is turned on', 37 | })) 38 | self.add_property( 39 | Property(self, 40 | 'brightness', 41 | Value(self.ledBrightness, self.setBrightness), 42 | metadata={ 43 | '@type': 'BrightnessProperty', 44 | 'title': 'Brightness', 45 | 'type': 'number', 46 | 'minimum': 0, 47 | 'maximum': 100, 48 | 'unit': 'percent', 49 | 'description': 'The brightness of the LED', 50 | })) 51 | 52 | def setOnOff(self, onOff): 53 | log.info('setOnOff: onOff = ' + str(onOff)) 54 | self.on = onOff 55 | self.updateLed() 56 | 57 | def setBrightness(self, brightness): 58 | log.info('setBrightness: brightness = ' + str(brightness)) 59 | self.ledBrightness = brightness 60 | self.updateLed() 61 | 62 | def updateLed(self): 63 | log.debug('updateLed: on = ' + str(self.on) + 64 | ' brightness = ' + str(self.ledBrightness)) 65 | if self.on: 66 | self.pwmLed.duty(self.ledBrightness) 67 | else: 68 | self.pwmLed.duty(0) 69 | 70 | 71 | class Button(Thing): 72 | 73 | def __init__(self, pin): 74 | Thing.__init__(self, 75 | 'Button 0', 76 | ['BinarySensor'], 77 | 'Button 0 on SparkFun ESP32 Thing') 78 | self.pin = machine.Pin(pin, machine.Pin.IN) 79 | 80 | self.button = Value(False) 81 | self.add_property( 82 | Property(self, 83 | 'on', 84 | self.button, 85 | metadata={ 86 | 'type': 'boolean', 87 | 'description': 'Button 0 pressed', 88 | 'readOnly': True, 89 | })) 90 | self.prev_pressed = self.is_pressed() 91 | 92 | def is_pressed(self): 93 | return self.pin.value() == 0 94 | 95 | def process(self): 96 | pressed = self.is_pressed() 97 | if pressed != self.prev_pressed: 98 | self.prev_pressed = pressed 99 | log.debug('pressed = ' + str(pressed)) 100 | self.button.notify_of_external_update(pressed) 101 | 102 | 103 | def run_server(): 104 | log.info('run_server') 105 | 106 | led = Led(5) 107 | button = Button(0) 108 | 109 | # If adding more than one thing here, be sure to set the `name` 110 | # parameter to some string, which will be broadcast via mDNS. 111 | # In the single thing case, the thing's name will be broadcast. 112 | server = WebThingServer(MultipleThings([led, button], 113 | 'SparkFun-ESP32-Thing'), 114 | port=80) 115 | try: 116 | log.info('starting the server') 117 | server.start() 118 | except KeyboardInterrupt: 119 | log.info('stopping the server') 120 | server.stop() 121 | log.info('done') 122 | 123 | while True: 124 | time.sleep(0.1) 125 | button.process() 126 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import connect 4 | 5 | logging.basicConfig(logging.DEBUG) 6 | log = logging.getLogger(__name__) 7 | 8 | sys.path.append('/flash/upy') 9 | sys.path.append('/flash/webthing') 10 | sys.path.append('/flash/example') 11 | 12 | connect.connect_to_ap() 13 | connect.start_ntp() 14 | 15 | 16 | def rgb(): 17 | print('importing esp32_wrover_kit_rgb...') 18 | import esp32_wrover_kit_rgb 19 | print('Starting esp32_wrover_kit_rgb server...') 20 | esp32_wrover_kit_rgb.run_server() 21 | 22 | 23 | def single(): 24 | print('importing single_thing...') 25 | import single_thing 26 | print('Starting single_thing server...') 27 | single_thing.run_server() 28 | 29 | 30 | def multi(): 31 | print('importing multiple_things...') 32 | import multiple_things 33 | print('Starting multiple_things server...') 34 | multiple_things.run_server() 35 | 36 | 37 | def thing(): 38 | print('importing sparkfun_esp32_thing...') 39 | import sparkfun_esp32_thing 40 | print('Starting sparkfun_esp32_thing server...') 41 | sparkfun_esp32_thing.run_server() 42 | -------------------------------------------------------------------------------- /upy/README.md: -------------------------------------------------------------------------------- 1 | The files in this directory are "micro" implementations of modules with 2 | the same name from CPython. -------------------------------------------------------------------------------- /upy/copy.py: -------------------------------------------------------------------------------- 1 | """Generic (shallow and deep) copying operations. 2 | 3 | Interface summary: 4 | 5 | import copy 6 | 7 | x = copy.copy(y) # make a shallow copy of y 8 | x = copy.deepcopy(y) # make a deep copy of y 9 | 10 | For module specific errors, copy.Error is raised. 11 | 12 | The difference between shallow and deep copying is only relevant for 13 | compound objects (objects that contain other objects, like lists or 14 | class instances). 15 | 16 | - A shallow copy constructs a new compound object and then (to the 17 | extent possible) inserts *the same objects* into it that the 18 | original contains. 19 | 20 | - A deep copy constructs a new compound object and then, recursively, 21 | inserts *copies* into it of the objects found in the original. 22 | 23 | Two problems often exist with deep copy operations that don't exist 24 | with shallow copy operations: 25 | 26 | a) recursive objects (compound objects that, directly or indirectly, 27 | contain a reference to themselves) may cause a recursive loop 28 | 29 | b) because deep copy copies *everything* it may copy too much, e.g. 30 | administrative data structures that should be shared even between 31 | copies 32 | 33 | Python's deep copy operation avoids these problems by: 34 | 35 | a) keeping a table of objects already copied during the current 36 | copying pass 37 | 38 | b) letting user-defined classes override the copying operation or the 39 | set of components copied 40 | 41 | This version does not copy types like module, class, function, method, 42 | nor stack trace, stack frame, nor file, socket, window, nor array, nor 43 | any similar types. 44 | 45 | Classes can use the same interfaces to control copying that they use 46 | to control pickling: they can define methods called __getinitargs__(), 47 | __getstate__() and __setstate__(). See the documentation for module 48 | "pickle" for information on these methods. 49 | """ 50 | 51 | import types 52 | # import weakref 53 | # from copyreg import dispatch_table 54 | # import builtins 55 | 56 | 57 | class Error(Exception): 58 | pass 59 | 60 | 61 | error = Error # backward compatibility 62 | 63 | try: 64 | from org.python.core import PyStringMap 65 | except ImportError: 66 | PyStringMap = None 67 | 68 | __all__ = ["Error", "copy", "deepcopy"] 69 | 70 | 71 | def copy(x): 72 | """Shallow copy operation on arbitrary Python objects. 73 | 74 | See the module's __doc__ string for more info. 75 | """ 76 | 77 | cls = type(x) 78 | 79 | copier = _copy_dispatch.get(cls) 80 | if copier: 81 | return copier(x) 82 | 83 | copier = getattr(cls, "__copy__", None) 84 | if copier: 85 | return copier(x) 86 | 87 | raise Error("un(shallow)copyable object of type %s" % cls) 88 | 89 | dispatch_table = {} 90 | reductor = dispatch_table.get(cls) 91 | if reductor: 92 | rv = reductor(x) 93 | else: 94 | reductor = getattr(x, "__reduce_ex__", None) 95 | if reductor: 96 | rv = reductor(2) 97 | else: 98 | reductor = getattr(x, "__reduce__", None) 99 | if reductor: 100 | rv = reductor() 101 | else: 102 | raise Error("un(shallow)copyable object of type %s" % cls) 103 | 104 | return _reconstruct(x, rv, 0) 105 | 106 | 107 | _copy_dispatch = d = {} 108 | 109 | 110 | def _copy_immutable(x): 111 | return x 112 | 113 | 114 | for t in (type(None), int, float, bool, str, tuple, 115 | type, range, 116 | types.BuiltinFunctionType, type(Ellipsis), 117 | types.FunctionType): 118 | d[t] = _copy_immutable 119 | t = getattr(types, "CodeType", None) 120 | if t is not None: 121 | d[t] = _copy_immutable 122 | # for name in ("complex", "unicode"): 123 | # t = getattr(builtins, name, None) 124 | # if t is not None: 125 | # d[t] = _copy_immutable 126 | 127 | 128 | def _copy_with_constructor(x): 129 | return type(x)(x) 130 | 131 | 132 | for t in (list, dict, set): 133 | d[t] = _copy_with_constructor 134 | 135 | 136 | def _copy_with_copy_method(x): 137 | return x.copy() 138 | 139 | 140 | if PyStringMap is not None: 141 | d[PyStringMap] = _copy_with_copy_method 142 | 143 | del d 144 | 145 | 146 | def deepcopy(x, memo=None, _nil=[]): 147 | """Deep copy operation on arbitrary Python objects. 148 | 149 | See the module's __doc__ string for more info. 150 | """ 151 | 152 | if memo is None: 153 | memo = {} 154 | 155 | d = id(x) 156 | y = memo.get(d, _nil) 157 | if y is not _nil: 158 | return y 159 | 160 | cls = type(x) 161 | 162 | copier = _deepcopy_dispatch.get(cls) 163 | if copier: 164 | y = copier(x, memo) 165 | else: 166 | try: 167 | issc = issubclass(cls, type) 168 | except TypeError: # cls is not a class (old Boost; see SF #502085) 169 | issc = 0 170 | if issc: 171 | y = _deepcopy_atomic(x, memo) 172 | else: 173 | copier = getattr(x, "__deepcopy__", None) 174 | if copier: 175 | y = copier(memo) 176 | else: 177 | reductor = dispatch_table.get(cls) 178 | if reductor: 179 | rv = reductor(x) 180 | else: 181 | reductor = getattr(x, "__reduce_ex__", None) 182 | if reductor: 183 | rv = reductor(2) 184 | else: 185 | reductor = getattr(x, "__reduce__", None) 186 | if reductor: 187 | rv = reductor() 188 | else: 189 | raise Error( 190 | "un(deep)copyable object of type %s" % cls) 191 | y = _reconstruct(x, rv, 1, memo) 192 | 193 | # If is its own copy, don't memoize. 194 | if y is not x: 195 | memo[d] = y 196 | _keep_alive(x, memo) # Make sure x lives at least as long as d 197 | return y 198 | 199 | 200 | _deepcopy_dispatch = d = {} 201 | 202 | 203 | def _deepcopy_atomic(x, memo): 204 | return x 205 | 206 | 207 | d[type(None)] = _deepcopy_atomic 208 | d[type(Ellipsis)] = _deepcopy_atomic 209 | d[int] = _deepcopy_atomic 210 | d[float] = _deepcopy_atomic 211 | d[bool] = _deepcopy_atomic 212 | try: 213 | d[complex] = _deepcopy_atomic 214 | except NameError: 215 | pass 216 | d[bytes] = _deepcopy_atomic 217 | d[str] = _deepcopy_atomic 218 | try: 219 | d[types.CodeType] = _deepcopy_atomic 220 | except AttributeError: 221 | pass 222 | d[type] = _deepcopy_atomic 223 | d[range] = _deepcopy_atomic 224 | d[types.BuiltinFunctionType] = _deepcopy_atomic 225 | d[types.FunctionType] = _deepcopy_atomic 226 | # d[weakref.ref] = _deepcopy_atomic 227 | 228 | 229 | def _deepcopy_list(x, memo): 230 | y = [] 231 | memo[id(x)] = y 232 | for a in x: 233 | y.append(deepcopy(a, memo)) 234 | return y 235 | 236 | 237 | d[list] = _deepcopy_list 238 | 239 | 240 | def _deepcopy_tuple(x, memo): 241 | y = [] 242 | for a in x: 243 | y.append(deepcopy(a, memo)) 244 | # We're not going to put the tuple in the memo, but it's still important we 245 | # check for it, in case the tuple contains recursive mutable structures. 246 | try: 247 | return memo[id(x)] 248 | except KeyError: 249 | pass 250 | for i in range(len(x)): 251 | if x[i] is not y[i]: 252 | y = tuple(y) 253 | break 254 | else: 255 | y = x 256 | return y 257 | 258 | 259 | d[tuple] = _deepcopy_tuple 260 | 261 | 262 | def _deepcopy_dict(x, memo): 263 | y = {} 264 | memo[id(x)] = y 265 | for key, value in x.items(): 266 | y[deepcopy(key, memo)] = deepcopy(value, memo) 267 | return y 268 | 269 | 270 | d[dict] = _deepcopy_dict 271 | if PyStringMap is not None: 272 | d[PyStringMap] = _deepcopy_dict 273 | 274 | 275 | def _deepcopy_method(x, memo): # Copy instance methods 276 | return type(x)(x.__func__, deepcopy(x.__self__, memo)) 277 | 278 | 279 | _deepcopy_dispatch[types.MethodType] = _deepcopy_method 280 | 281 | 282 | def _keep_alive(x, memo): 283 | """Keeps a reference to the object x in the memo. 284 | 285 | Because we remember objects by their id, we have 286 | to assure that possibly temporary objects are kept 287 | alive by referencing them. 288 | We store a reference at the id of the memo, which should 289 | normally not be used unless someone tries to deepcopy 290 | the memo itself... 291 | """ 292 | try: 293 | memo[id(memo)].append(x) 294 | except KeyError: 295 | # aha, this is the first one :-) 296 | memo[id(memo)] = [x] 297 | 298 | 299 | def _reconstruct(x, info, deep, memo=None): 300 | if isinstance(info, str): 301 | return x 302 | assert isinstance(info, tuple) 303 | if memo is None: 304 | memo = {} 305 | n = len(info) 306 | assert n in (2, 3, 4, 5) 307 | callable, args = info[:2] 308 | if n > 2: 309 | state = info[2] 310 | else: 311 | state = {} 312 | if n > 3: 313 | listiter = info[3] 314 | else: 315 | listiter = None 316 | if n > 4: 317 | dictiter = info[4] 318 | else: 319 | dictiter = None 320 | if deep: 321 | args = deepcopy(args, memo) 322 | y = callable(*args) 323 | memo[id(x)] = y 324 | 325 | if state: 326 | if deep: 327 | state = deepcopy(state, memo) 328 | if hasattr(y, '__setstate__'): 329 | y.__setstate__(state) 330 | else: 331 | if isinstance(state, tuple) and len(state) == 2: 332 | state, slotstate = state 333 | else: 334 | slotstate = None 335 | if state is not None: 336 | y.__dict__.update(state) 337 | if slotstate is not None: 338 | for key, value in slotstate.items(): 339 | setattr(y, key, value) 340 | 341 | if listiter is not None: 342 | for item in listiter: 343 | if deep: 344 | item = deepcopy(item, memo) 345 | y.append(item) 346 | if dictiter is not None: 347 | for key, value in dictiter: 348 | if deep: 349 | key = deepcopy(key, memo) 350 | value = deepcopy(value, memo) 351 | y[key] = value 352 | return y 353 | 354 | 355 | del d 356 | 357 | del types 358 | 359 | 360 | # Helper for instance creation without calling __init__ 361 | class _EmptyClass: 362 | pass 363 | -------------------------------------------------------------------------------- /upy/eventemitter.py: -------------------------------------------------------------------------------- 1 | '''A super simple EventEmitter implementation. 2 | 3 | Modified slightly from: https://github.com/axetroy/pyee 4 | ''' 5 | 6 | 7 | class EventEmitter: 8 | 9 | def __init__(self): 10 | self._events = {} 11 | 12 | def on(self, event, handler): 13 | events = self._events 14 | if event not in events: 15 | events[event] = [] 16 | events[event].append(handler) 17 | 18 | def emit(self, event, *data): 19 | events = self._events 20 | if event not in events: 21 | return 22 | handlers = events[event] 23 | for handler in handlers: 24 | handler(data) 25 | -------------------------------------------------------------------------------- /upy/logging.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | CRITICAL = 50 4 | ERROR = 40 5 | WARNING = 30 6 | INFO = 20 7 | DEBUG = 10 8 | NOTSET = 0 9 | 10 | _level_dict = { 11 | CRITICAL: "CRIT", 12 | ERROR: "ERROR", 13 | WARNING: "WARN", 14 | INFO: "INFO", 15 | DEBUG: "DEBUG", 16 | } 17 | 18 | _stream = sys.stderr 19 | 20 | 21 | class Logger: 22 | 23 | def __init__(self, name): 24 | self.level = NOTSET 25 | self.name = name 26 | 27 | def _level_str(self, level): 28 | if level in _level_dict: 29 | return _level_dict[level] 30 | return "LVL" + str(level) 31 | 32 | def log(self, level, msg, *args): 33 | if level >= (self.level or _level): 34 | print(("%s:%s:" + msg) % 35 | ((self._level_str(level), self.name) + args), file=_stream) 36 | 37 | def debug(self, msg, *args): 38 | self.log(DEBUG, msg, *args) 39 | 40 | def info(self, msg, *args): 41 | self.log(INFO, msg, *args) 42 | 43 | def warning(self, msg, *args): 44 | self.log(WARNING, msg, *args) 45 | 46 | def error(self, msg, *args): 47 | self.log(ERROR, msg, *args) 48 | 49 | def critical(self, msg, *args): 50 | self.log(CRITICAL, msg, *args) 51 | 52 | 53 | _level = INFO 54 | _loggers = {} 55 | 56 | 57 | def getLogger(name): 58 | if name in _loggers: 59 | return _loggers[name] 60 | logger = Logger(name) 61 | _loggers[name] = logger 62 | return logger 63 | 64 | 65 | def info(msg, *args): 66 | getLogger(None).info(msg, *args) 67 | 68 | 69 | def debug(msg, *args): 70 | getLogger(None).debug(msg, *args) 71 | 72 | 73 | def basicConfig(level=INFO, filename=None, stream=None, format=None): 74 | global _level, _stream 75 | _level = level 76 | if stream: 77 | _stream = stream 78 | if filename is not None: 79 | print("logging.basicConfig: filename arg is not supported") 80 | if format is not None: 81 | print("logging.basicConfig: format arg is not supported") 82 | -------------------------------------------------------------------------------- /upy/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Define names for built-in types that aren't directly accessible as a builtin. 3 | """ 4 | import sys 5 | 6 | # Iterators in Python aren't a matter of type but of protocol. A large 7 | # and changing number of builtin types implement *some* flavor of 8 | # iterator. Don't check the type! Use hasattr to check for both 9 | # "__iter__" and "__next__" attributes instead. 10 | 11 | 12 | def _f(): pass 13 | 14 | 15 | FunctionType = type(_f) 16 | LambdaType = type(lambda: None) # Same as FunctionType 17 | CodeType = None # TODO: Add better sentinel which can't match anything 18 | MappingProxyType = None # TODO: Add better sentinel which can't match anything 19 | SimpleNamespace = None # TODO: Add better sentinel which can't match anything 20 | 21 | 22 | def _g(): 23 | yield 1 24 | 25 | 26 | GeneratorType = type(_g()) 27 | 28 | 29 | class _C: 30 | def _m(self): pass 31 | 32 | 33 | MethodType = type(_C()._m) 34 | 35 | BuiltinFunctionType = type(len) 36 | BuiltinMethodType = type([].append) # Same as BuiltinFunctionType 37 | 38 | ModuleType = type(sys) 39 | 40 | try: 41 | raise TypeError 42 | except TypeError: 43 | # tb = sys.exc_info()[2] 44 | # TODO: Add better sentinel which can't match anything 45 | TracebackType = None 46 | # TODO: Add better sentinel which can't match anything 47 | FrameType = None 48 | tb = None 49 | del tb 50 | 51 | # For Jython, the following two types are identical 52 | # TODO: Add better sentinel which can't match anything 53 | GetSetDescriptorType = None 54 | # TODO: Add better sentinel which can't match anything 55 | MemberDescriptorType = None 56 | 57 | del sys, _f, _g, _C, # Not for export 58 | 59 | 60 | # Provide a PEP 3115 compliant mechanism for class creation 61 | def new_class(name, bases=(), kwds=None, exec_body=None): 62 | """Create a class object dynamically using the appropriate metaclass.""" 63 | meta, ns, kwds = prepare_class(name, bases, kwds) 64 | if exec_body is not None: 65 | exec_body(ns) 66 | return meta(name, bases, ns, **kwds) 67 | 68 | 69 | def prepare_class(name, bases=(), kwds=None): 70 | """Call the __prepare__ method of the appropriate metaclass. 71 | 72 | Returns (metaclass, namespace, kwds) as a 3-tuple 73 | 74 | *metaclass* is the appropriate metaclass 75 | *namespace* is the prepared class namespace 76 | *kwds* is an updated copy of the passed in kwds argument with any 77 | 'metaclass' entry removed. If no kwds argument is passed in, this will 78 | be an empty dict. 79 | """ 80 | if kwds is None: 81 | kwds = {} 82 | else: 83 | kwds = dict(kwds) # Don't alter the provided mapping 84 | if 'metaclass' in kwds: 85 | meta = kwds.pop('metaclass') 86 | else: 87 | if bases: 88 | meta = type(bases[0]) 89 | else: 90 | meta = type 91 | if isinstance(meta, type): 92 | # when meta is a type, we first determine the most-derived metaclass 93 | # instead of invoking the initial candidate directly 94 | meta = _calculate_meta(meta, bases) 95 | if hasattr(meta, '__prepare__'): 96 | ns = meta.__prepare__(name, bases, **kwds) 97 | else: 98 | ns = {} 99 | return meta, ns, kwds 100 | 101 | 102 | def _calculate_meta(meta, bases): 103 | """Calculate the most derived metaclass.""" 104 | winner = meta 105 | for base in bases: 106 | base_meta = type(base) 107 | if issubclass(winner, base_meta): 108 | continue 109 | if issubclass(base_meta, winner): 110 | winner = base_meta 111 | continue 112 | # else: 113 | raise TypeError("metaclass conflict: " 114 | "the metaclass of a derived class " 115 | "must be a (non-strict) subclass " 116 | "of the metaclasses of all its bases") 117 | return winner 118 | -------------------------------------------------------------------------------- /upy/uuid.py: -------------------------------------------------------------------------------- 1 | '''Implements just enough of UUID so that uuid4().hex will produce 2 | something usable. 3 | ''' 4 | 5 | import os 6 | 7 | 8 | class UUID: 9 | 10 | def __init__(self, bytes=None): 11 | if bytes is None: 12 | raise TypeError('Must provide bytes argument') 13 | if len(bytes) != 16: 14 | raise ValueError('bytes must be a 16-char string') 15 | self._int = int.from_bytes(bytes, 'big') 16 | 17 | def __str__(self): 18 | hex = '%032x' % self._int 19 | return '%s-%s-%s-%s-%s' % ( 20 | hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:]) 21 | 22 | @property 23 | def hex(self): 24 | return '%032x' % self._int 25 | 26 | 27 | def uuid4(): 28 | '''Generates a random UUID.''' 29 | return UUID(bytes=os.urandom(16)) 30 | -------------------------------------------------------------------------------- /webthing/action.py: -------------------------------------------------------------------------------- 1 | """High-level Action base class implementation.""" 2 | 3 | from utils import timestamp 4 | 5 | 6 | class Action: 7 | """An Action represents an individual action on a thing.""" 8 | 9 | def __init__(self, id_, thing, name, input_): 10 | """ 11 | Initialize the object. 12 | 13 | id_ ID of this action 14 | thing -- the Thing this action belongs to 15 | name -- name of the action 16 | input_ -- any action inputs 17 | """ 18 | self.id = id_ 19 | self.thing = thing 20 | self.name = name 21 | self.input = input_ 22 | self.href_prefix = '' 23 | self.href = '/actions/{}/{}'.format(self.name, self.id) 24 | self.status = 'created' 25 | self.time_requested = timestamp() 26 | self.time_completed = None 27 | 28 | def as_action_description(self): 29 | """ 30 | Get the action description. 31 | 32 | Returns a dictionary describing the action. 33 | """ 34 | description = { 35 | self.name: { 36 | 'href': self.href_prefix + self.href, 37 | 'timeRequested': self.time_requested, 38 | 'status': self.status, 39 | }, 40 | } 41 | 42 | if self.input is not None: 43 | description[self.name]['input'] = self.input 44 | 45 | if self.time_completed is not None: 46 | description[self.name]['timeCompleted'] = self.time_completed 47 | 48 | return description 49 | 50 | def set_href_prefix(self, prefix): 51 | """ 52 | Set the prefix of any hrefs associated with this action. 53 | 54 | prefix -- the prefix 55 | """ 56 | self.href_prefix = prefix 57 | 58 | def get_id(self): 59 | """Get this action's ID.""" 60 | return self.id 61 | 62 | def get_name(self): 63 | """Get this action's name.""" 64 | return self.name 65 | 66 | def get_href(self): 67 | """Get this action's href.""" 68 | return self.href_prefix + self.href 69 | 70 | def get_status(self): 71 | """Get this action's status.""" 72 | return self.status 73 | 74 | def get_thing(self): 75 | """Get the thing associated with this action.""" 76 | return self.thing 77 | 78 | def get_time_requested(self): 79 | """Get the time the action was requested.""" 80 | return self.time_requested 81 | 82 | def get_time_completed(self): 83 | """Get the time the action was completed.""" 84 | return self.time_completed 85 | 86 | def get_input(self): 87 | """Get the inputs for this action.""" 88 | return self.input 89 | 90 | def start(self): 91 | """Start performing the action.""" 92 | self.status = 'pending' 93 | self.thing.action_notify(self) 94 | self.perform_action() 95 | self.finish() 96 | 97 | def perform_action(self): 98 | """Override this with the code necessary to perform the action.""" 99 | pass 100 | 101 | def cancel(self): 102 | """Override this with the code necessary to cancel the action.""" 103 | pass 104 | 105 | def finish(self): 106 | """Finish performing the action.""" 107 | self.status = 'completed' 108 | self.time_completed = timestamp() 109 | self.thing.action_notify(self) 110 | -------------------------------------------------------------------------------- /webthing/errors.py: -------------------------------------------------------------------------------- 1 | """Exception types.""" 2 | 3 | 4 | class PropertyError(Exception): 5 | """Exception to indicate an issue with a property.""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /webthing/event.py: -------------------------------------------------------------------------------- 1 | """High-level Event base class implementation.""" 2 | 3 | from utils import timestamp 4 | 5 | 6 | class Event: 7 | """An Event represents an individual event from a thing.""" 8 | 9 | def __init__(self, thing, name, data=None): 10 | """ 11 | Initialize the object. 12 | 13 | thing -- Thing this event belongs to 14 | name -- name of the event 15 | data -- data associated with the event 16 | """ 17 | self.thing = thing 18 | self.name = name 19 | self.data = data 20 | self.time = timestamp() 21 | 22 | def as_event_description(self): 23 | """ 24 | Get the event description. 25 | 26 | Returns a dictionary describing the event. 27 | """ 28 | description = { 29 | self.name: { 30 | 'timestamp': self.time, 31 | }, 32 | } 33 | 34 | if self.data is not None: 35 | description[self.name]['data'] = self.data 36 | 37 | return description 38 | 39 | def get_thing(self): 40 | """Get the thing associated with this event.""" 41 | return self.thing 42 | 43 | def get_name(self): 44 | """Get the event's name.""" 45 | return self.name 46 | 47 | def get_data(self): 48 | """Get the event's data.""" 49 | return self.data 50 | 51 | def get_time(self): 52 | """Get the event's timestamp.""" 53 | return self.time 54 | -------------------------------------------------------------------------------- /webthing/property.py: -------------------------------------------------------------------------------- 1 | """High-level Property base class implementation.""" 2 | 3 | from copy import deepcopy 4 | 5 | from errors import PropertyError 6 | 7 | 8 | class Property: 9 | """A Property represents an individual state value of a thing.""" 10 | 11 | def __init__(self, thing, name, value, metadata=None): 12 | """ 13 | Initialize the object. 14 | 15 | thing -- the Thing this property belongs to 16 | name -- name of the property 17 | value -- Value object to hold the property value 18 | metadata -- property metadata, i.e. type, description, unit, etc., 19 | as a dict 20 | """ 21 | self.thing = thing 22 | self.name = name 23 | self.value = value 24 | self.href_prefix = '' 25 | self.href = '/properties/{}'.format(self.name) 26 | self.metadata = metadata if metadata is not None else {} 27 | 28 | # Add the property change observer to notify the Thing about a property 29 | # change. 30 | self.value.on('update', lambda _: self.thing.property_notify(self)) 31 | 32 | def validate_value(self, value): 33 | """ 34 | Validate new property value before setting it. 35 | 36 | value -- New value 37 | """ 38 | if 'type' in self.metadata: 39 | t = self.metadata['type'] 40 | 41 | if t == 'null': 42 | if t is not None: 43 | raise PropertyError('Value must be null') 44 | elif t == 'boolean': 45 | if type(value) is not bool: 46 | raise PropertyError('Value must be a boolean') 47 | elif t == 'object': 48 | if type(value) is not dict: 49 | raise PropertyError('Value must be an object') 50 | elif t == 'array': 51 | if type(value) is not list: 52 | raise PropertyError('Value must be an array') 53 | elif t == 'number': 54 | if type(value) not in [float, int]: 55 | raise PropertyError('Value must be a number') 56 | elif t == 'integer': 57 | if type(value) is not int: 58 | raise PropertyError('Value must be an integer') 59 | elif t == 'string': 60 | if type(value) is not str: 61 | raise PropertyError('Value must be a string') 62 | 63 | if 'readOnly' in self.metadata and self.metadata['readOnly']: 64 | raise PropertyError('Read-only property') 65 | 66 | if 'minimum' in self.metadata and value < self.metadata['minimum']: 67 | raise PropertyError('Value less than minimum: {}' 68 | .format(self.metadata['minimum'])) 69 | 70 | if 'maximum' in self.metadata and value > self.metadata['maximum']: 71 | raise PropertyError('Value greater than maximum: {}' 72 | .format(self.metadata['maximum'])) 73 | 74 | if 'enum' in self.metadata and len(self.metadata['enum']) > 0 and \ 75 | value not in self.metadata['enum']: 76 | raise PropertyError('Invalid enum value') 77 | 78 | def as_property_description(self): 79 | """ 80 | Get the property description. 81 | 82 | Returns a dictionary describing the property. 83 | """ 84 | description = deepcopy(self.metadata) 85 | 86 | if 'links' not in description: 87 | description['links'] = [] 88 | 89 | description['links'].append( 90 | { 91 | 'rel': 'property', 92 | 'href': self.href_prefix + self.href, 93 | } 94 | ) 95 | return description 96 | 97 | def set_href_prefix(self, prefix): 98 | """ 99 | Set the prefix of any hrefs associated with this property. 100 | 101 | prefix -- the prefix 102 | """ 103 | self.href_prefix = prefix 104 | 105 | def get_href(self): 106 | """ 107 | Get the href of this property. 108 | 109 | Returns the href. 110 | """ 111 | return self.href_prefix + self.href 112 | 113 | def get_value(self): 114 | """ 115 | Get the current property value. 116 | 117 | Returns the value. 118 | """ 119 | return self.value.get() 120 | 121 | def set_value(self, value): 122 | """ 123 | Set the current value of the property. 124 | 125 | value -- the value to set 126 | """ 127 | self.validate_value(value) 128 | self.value.set(value) 129 | 130 | def get_name(self): 131 | """ 132 | Get the name of this property. 133 | 134 | Returns the name. 135 | """ 136 | return self.name 137 | 138 | def get_thing(self): 139 | """Get the thing associated with this property.""" 140 | return self.thing 141 | 142 | def get_metadata(self): 143 | """Get the metadata associated with this property.""" 144 | return self.metadata 145 | -------------------------------------------------------------------------------- /webthing/server.py: -------------------------------------------------------------------------------- 1 | """Python Web Thing server implementation.""" 2 | 3 | from microWebSrv import MicroWebSrv 4 | import _thread 5 | import logging 6 | import sys 7 | import network 8 | 9 | from errors import PropertyError 10 | from utils import get_addresses 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | # set to True to print WebSocket messages 15 | WS_messages = True 16 | 17 | # ================================================= 18 | # Recommended configuration: 19 | # - run microWebServer in thread 20 | # - do NOT run MicroWebSocket in thread 21 | # ================================================= 22 | # Run microWebServer in thread 23 | srv_run_in_thread = True 24 | # Run microWebSocket in thread 25 | ws_run_in_thread = False 26 | 27 | _CORS_HEADERS = { 28 | 'Access-Control-Allow-Origin': '*', 29 | 'Access-Control-Allow-Headers': 30 | 'Origin, X-Requested-With, Content-Type, Accept', 31 | 'Access-Control-Allow-Methods': 'GET, HEAD, PUT, POST, DELETE', 32 | } 33 | 34 | 35 | def print_exc(func): 36 | """Wrap a function and print an exception, if encountered.""" 37 | def wrapper(*args, **kwargs): 38 | try: 39 | # log.debug('Calling {}'.format(func.__name__)) 40 | ret = func(*args, **kwargs) 41 | # log.debug('Back from {}'.format(func.__name__)) 42 | return ret 43 | except Exception as err: 44 | sys.print_exception(err) 45 | return wrapper 46 | 47 | 48 | class SingleThing: 49 | """A container for a single thing.""" 50 | 51 | def __init__(self, thing): 52 | """ 53 | Initialize the container. 54 | 55 | thing -- the thing to store 56 | """ 57 | self.thing = thing 58 | 59 | def get_thing(self, _=None): 60 | """Get the thing at the given index.""" 61 | return self.thing 62 | 63 | def get_things(self): 64 | """Get the list of things.""" 65 | return [self.thing] 66 | 67 | def get_name(self): 68 | """Get the mDNS server name.""" 69 | return self.thing.title 70 | 71 | 72 | class MultipleThings: 73 | """A container for multiple things.""" 74 | 75 | def __init__(self, things, name): 76 | """ 77 | Initialize the container. 78 | 79 | things -- the things to store 80 | name -- the mDNS server name 81 | """ 82 | self.things = things 83 | self.name = name 84 | 85 | def get_thing(self, idx): 86 | """ 87 | Get the thing at the given index. 88 | 89 | idx -- the index 90 | """ 91 | try: 92 | idx = int(idx) 93 | except ValueError: 94 | return None 95 | 96 | if idx < 0 or idx >= len(self.things): 97 | return None 98 | 99 | return self.things[idx] 100 | 101 | def get_things(self): 102 | """Get the list of things.""" 103 | return self.things 104 | 105 | def get_name(self): 106 | """Get the mDNS server name.""" 107 | return self.name 108 | 109 | 110 | class WebThingServer: 111 | """Server to represent a Web Thing over HTTP.""" 112 | 113 | def __init__(self, things, port=80, hostname=None, ssl_options=None, 114 | additional_routes=None, base_path='', 115 | disable_host_validation=False): 116 | """ 117 | Initialize the WebThingServer. 118 | 119 | For documentation on the additional route format, see: 120 | https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/microWebSrv 121 | 122 | things -- things managed by this server -- should be of type 123 | SingleThing or MultipleThings 124 | port -- port to listen on (defaults to 80) 125 | hostname -- Optional host name, i.e. mything.com 126 | ssl_options -- dict of SSL options to pass to the tornado server 127 | additional_routes -- list of additional routes to add to the server 128 | base_path -- base URL path to use, rather than '/' 129 | disable_host_validation -- whether or not to disable host validation -- 130 | note that this can lead to DNS rebinding 131 | attacks 132 | """ 133 | self.ssl_suffix = '' if ssl_options is None else 's' 134 | 135 | self.things = things 136 | self.name = things.get_name() 137 | self.port = port 138 | self.hostname = hostname 139 | self.base_path = base_path.rstrip('/') 140 | self.disable_host_validation = disable_host_validation 141 | 142 | station = network.WLAN() 143 | mac = station.config('mac') 144 | self.system_hostname = 'esp32-upy-{:02x}{:02x}{:02x}'.format( 145 | mac[3], mac[4], mac[5]) 146 | 147 | self.hosts = [ 148 | 'localhost', 149 | 'localhost:{}'.format(self.port), 150 | '{}.local'.format(self.system_hostname), 151 | '{}.local:{}'.format(self.system_hostname, self.port), 152 | ] 153 | 154 | for address in get_addresses(): 155 | self.hosts.extend([ 156 | address, 157 | '{}:{}'.format(address, self.port), 158 | ]) 159 | 160 | if self.hostname is not None: 161 | self.hostname = self.hostname.lower() 162 | self.hosts.extend([ 163 | self.hostname, 164 | '{}:{}'.format(self.hostname, self.port), 165 | ]) 166 | 167 | if isinstance(self.things, MultipleThings): 168 | for idx, thing in enumerate(self.things.get_things()): 169 | thing.set_href_prefix('{}/{}'.format(self.base_path, idx)) 170 | 171 | handlers = [ 172 | [ 173 | '/.*', 174 | 'OPTIONS', 175 | self.optionsHandler 176 | ], 177 | [ 178 | '/', 179 | 'GET', 180 | self.thingsGetHandler 181 | ], 182 | [ 183 | '/', 184 | 'GET', 185 | self.thingGetHandler 186 | ], 187 | [ 188 | '//properties', 189 | 'GET', 190 | self.propertiesGetHandler 191 | ], 192 | [ 193 | '//properties/', 194 | 'GET', 195 | self.propertyGetHandler 196 | ], 197 | [ 198 | '//properties/', 199 | 'PUT', 200 | self.propertyPutHandler 201 | ], 202 | ] 203 | else: 204 | self.things.get_thing().set_href_prefix(self.base_path) 205 | handlers = [ 206 | [ 207 | '/.*', 208 | 'OPTIONS', 209 | self.optionsHandler 210 | ], 211 | [ 212 | '/', 213 | 'GET', 214 | self.thingGetHandler 215 | ], 216 | [ 217 | '/properties', 218 | 'GET', 219 | self.propertiesGetHandler 220 | ], 221 | [ 222 | '/properties/', 223 | 'GET', 224 | self.propertyGetHandler 225 | ], 226 | [ 227 | '/properties/', 228 | 'PUT', 229 | self.propertyPutHandler 230 | ], 231 | ] 232 | 233 | if isinstance(additional_routes, list): 234 | handlers = additional_routes + handlers 235 | 236 | if self.base_path: 237 | for h in handlers: 238 | h[0] = self.base_path + h[0] 239 | 240 | self.server = MicroWebSrv(webPath='/flash/www', 241 | routeHandlers=handlers, 242 | port=port) 243 | self.server.MaxWebSocketRecvLen = 256 244 | self.WebSocketThreaded = ws_run_in_thread 245 | self.server.WebSocketStackSize = 8 * 1024 246 | self.server.AcceptWebSocketCallback = self._acceptWebSocketCallback 247 | 248 | def start(self): 249 | """Start listening for incoming connections.""" 250 | # If WebSocketS used and NOT running in thread, and WebServer IS 251 | # running in thread make shure WebServer has enough stack size to 252 | # handle also the WebSocket requests. 253 | log.info('Starting Web Server on port {}'.format(self.port)) 254 | self.server.Start(threaded=srv_run_in_thread, stackSize=12*1024) 255 | 256 | mdns = network.mDNS() 257 | mdns.start(self.system_hostname, 'MicroPython with mDNS') 258 | mdns.addService('_webthing', '_tcp', 80, self.system_hostname, 259 | { 260 | 'board': 'ESP32', 261 | 'path': '/', 262 | }) 263 | 264 | def stop(self): 265 | """Stop listening.""" 266 | self.server.Stop() 267 | 268 | def getThing(self, routeArgs): 269 | """Get the thing ID based on the route.""" 270 | if not routeArgs or 'thing_id' not in routeArgs: 271 | thing_id = None 272 | else: 273 | thing_id = routeArgs['thing_id'] 274 | 275 | return self.things.get_thing(thing_id) 276 | 277 | def getProperty(self, routeArgs): 278 | """Get the property name based on the route.""" 279 | thing = self.getThing(routeArgs) 280 | if thing: 281 | property_name = routeArgs['property_name'] 282 | if thing.has_property(property_name): 283 | return thing, thing.find_property(property_name) 284 | return None, None 285 | 286 | def getHeader(self, headers, key, default=None): 287 | standardized = {k.lower(): v for k, v in headers.items()} 288 | return standardized.get(key, default) 289 | 290 | def validateHost(self, headers): 291 | """Validate the Host header in the request.""" 292 | host = self.getHeader(headers, 'host') 293 | if self.disable_host_validation or ( 294 | host is not None and host.lower() in self.hosts): 295 | return True 296 | 297 | return False 298 | 299 | @print_exc 300 | def optionsHandler(self, httpClient, httpResponse, routeArgs=None): 301 | """Handle an OPTIONS request to any path.""" 302 | if not self.validateHost(httpClient.GetRequestHeaders()): 303 | httpResponse.WriteResponseError(403) 304 | return 305 | 306 | httpResponse.WriteResponse(204, _CORS_HEADERS, None, None, None) 307 | 308 | @print_exc 309 | def thingsGetHandler(self, httpClient, httpResponse): 310 | """Handle a request to / when the server manages multiple things.""" 311 | if not self.validateHost(httpClient.GetRequestHeaders()): 312 | httpResponse.WriteResponseError(403) 313 | return 314 | 315 | headers = httpClient.GetRequestHeaders() 316 | base_href = 'http{}://{}'.format( 317 | self.ssl_suffix, 318 | self.getHeader(headers, 'host', '') 319 | ) 320 | ws_href = 'ws{}://{}'.format( 321 | self.ssl_suffix, 322 | self.getHeader(headers, 'host', '') 323 | ) 324 | 325 | descriptions = [] 326 | for thing in self.things.get_things(): 327 | description = thing.as_thing_description() 328 | description['links'].append({ 329 | 'rel': 'alternate', 330 | 'href': '{}{}'.format(ws_href, thing.get_href()), 331 | }) 332 | description['href'] = thing.get_href() 333 | description['base'] = '{}{}'.format(base_href, thing.get_href()) 334 | description['securityDefinitions'] = { 335 | 'nosec_sc': { 336 | 'scheme': 'nosec', 337 | }, 338 | } 339 | description['security'] = 'nosec_sc' 340 | descriptions.append(description) 341 | 342 | httpResponse.WriteResponseJSONOk( 343 | obj=descriptions, 344 | headers=_CORS_HEADERS, 345 | ) 346 | 347 | @print_exc 348 | def thingGetHandler(self, httpClient, httpResponse, routeArgs=None): 349 | """Handle a GET request for an individual thing.""" 350 | if not self.validateHost(httpClient.GetRequestHeaders()): 351 | httpResponse.WriteResponseError(403) 352 | return 353 | 354 | thing = self.getThing(routeArgs) 355 | if thing is None: 356 | httpResponse.WriteResponseNotFound() 357 | return 358 | 359 | headers = httpClient.GetRequestHeaders() 360 | base_href = 'http{}://{}'.format( 361 | self.ssl_suffix, 362 | self.getHeader(headers, 'host', '') 363 | ) 364 | ws_href = 'ws{}://{}'.format( 365 | self.ssl_suffix, 366 | self.getHeader(headers, 'host', '') 367 | ) 368 | 369 | description = thing.as_thing_description() 370 | description['links'].append({ 371 | 'rel': 'alternate', 372 | 'href': '{}{}'.format(ws_href, thing.get_href()), 373 | }) 374 | description['base'] = '{}{}'.format(base_href, thing.get_href()) 375 | description['securityDefinitions'] = { 376 | 'nosec_sc': { 377 | 'scheme': 'nosec', 378 | }, 379 | } 380 | description['security'] = 'nosec_sc' 381 | 382 | httpResponse.WriteResponseJSONOk( 383 | obj=description, 384 | headers=_CORS_HEADERS, 385 | ) 386 | 387 | @print_exc 388 | def propertiesGetHandler(self, httpClient, httpResponse, routeArgs=None): 389 | """Handle a GET request for a property.""" 390 | thing = self.getThing(routeArgs) 391 | if thing is None: 392 | httpResponse.WriteResponseNotFound() 393 | return 394 | httpResponse.WriteResponseJSONOk(thing.get_properties()) 395 | 396 | @print_exc 397 | def propertyGetHandler(self, httpClient, httpResponse, routeArgs=None): 398 | """Handle a GET request for a property.""" 399 | if not self.validateHost(httpClient.GetRequestHeaders()): 400 | httpResponse.WriteResponseError(403) 401 | return 402 | 403 | thing, prop = self.getProperty(routeArgs) 404 | if thing is None or prop is None: 405 | httpResponse.WriteResponseNotFound() 406 | return 407 | httpResponse.WriteResponseJSONOk( 408 | obj={prop.get_name(): prop.get_value()}, 409 | headers=_CORS_HEADERS, 410 | ) 411 | 412 | @print_exc 413 | def propertyPutHandler(self, httpClient, httpResponse, routeArgs=None): 414 | """Handle a PUT request for a property.""" 415 | if not self.validateHost(httpClient.GetRequestHeaders()): 416 | httpResponse.WriteResponseError(403) 417 | return 418 | 419 | thing, prop = self.getProperty(routeArgs) 420 | if thing is None or prop is None: 421 | httpResponse.WriteResponseNotFound() 422 | return 423 | args = httpClient.ReadRequestContentAsJSON() 424 | if args is None: 425 | httpResponse.WriteResponseBadRequest() 426 | return 427 | try: 428 | prop.set_value(args[prop.get_name()]) 429 | except PropertyError: 430 | httpResponse.WriteResponseBadRequest() 431 | return 432 | httpResponse.WriteResponseJSONOk( 433 | obj={prop.get_name(): prop.get_value()}, 434 | headers=_CORS_HEADERS, 435 | ) 436 | 437 | # === MicroWebSocket callbacks === 438 | 439 | @print_exc 440 | def _acceptWebSocketCallback(self, webSocket, httpClient): 441 | reqPath = httpClient.GetRequestPath() 442 | if WS_messages: 443 | print('WS ACCEPT reqPath =', reqPath) 444 | if ws_run_in_thread or srv_run_in_thread: 445 | # Print thread list so that we can monitor maximum stack size 446 | # of WebServer thread and WebSocket thread if any is used 447 | _thread.list() 448 | webSocket.RecvTextCallback = self._recvTextCallback 449 | webSocket.RecvBinaryCallback = self._recvBinaryCallback 450 | webSocket.ClosedCallback = self._closedCallback 451 | things = self.things.get_things() 452 | if len(things) == 1: 453 | thing_id = 0 454 | else: 455 | thing_id = int(reqPath.split('/')[1]) 456 | thing = things[thing_id] 457 | webSocket.thing = thing 458 | thing.add_subscriber(webSocket) 459 | 460 | @print_exc 461 | def _recvTextCallback(self, webSocket, msg): 462 | if WS_messages: 463 | print('WS RECV TEXT : %s' % msg) 464 | 465 | @print_exc 466 | def _recvBinaryCallback(self, webSocket, data): 467 | if WS_messages: 468 | print('WS RECV DATA : %s' % data) 469 | 470 | @print_exc 471 | def _closedCallback(self, webSocket): 472 | if WS_messages: 473 | if ws_run_in_thread or srv_run_in_thread: 474 | _thread.list() 475 | print('WS CLOSED') 476 | -------------------------------------------------------------------------------- /webthing/thing.py: -------------------------------------------------------------------------------- 1 | """High-level Thing base class implementation.""" 2 | 3 | import json 4 | 5 | 6 | class Thing: 7 | """A Web Thing.""" 8 | 9 | def __init__(self, id_, title, type_=[], description=''): 10 | """ 11 | Initialize the object. 12 | 13 | id_ -- the thing's unique ID - must be a URI 14 | title -- the thing's title 15 | type_ -- the thing's type(s) 16 | description -- description of the thing 17 | """ 18 | if not isinstance(type_, list): 19 | type_ = [type_] 20 | 21 | self.id = id_ 22 | self.context = 'https://webthings.io/schemas' 23 | self.type = type_ 24 | self.title = title 25 | self.description = description 26 | self.properties = {} 27 | self.available_actions = {} 28 | self.available_events = {} 29 | self.actions = {} 30 | self.events = [] 31 | self.subscribers = set() 32 | self.href_prefix = '' 33 | self.ui_href = None 34 | 35 | def as_thing_description(self): 36 | """ 37 | Return the thing state as a Thing Description. 38 | 39 | Returns the state as a dictionary. 40 | """ 41 | thing = { 42 | 'id': self.id, 43 | 'title': self.title, 44 | '@context': self.context, 45 | '@type': self.type, 46 | 'properties': self.get_property_descriptions(), 47 | 'actions': {}, 48 | 'events': {}, 49 | 'links': [ 50 | { 51 | 'rel': 'properties', 52 | 'href': '{}/properties'.format(self.href_prefix), 53 | }, 54 | { 55 | 'rel': 'actions', 56 | 'href': '{}/actions'.format(self.href_prefix), 57 | }, 58 | { 59 | 'rel': 'events', 60 | 'href': '{}/events'.format(self.href_prefix), 61 | }, 62 | ], 63 | } 64 | 65 | for name, action in self.available_actions.items(): 66 | thing['actions'][name] = action['metadata'] 67 | thing['actions'][name]['links'] = [ 68 | { 69 | 'rel': 'action', 70 | 'href': '{}/actions/{}'.format(self.href_prefix, name), 71 | }, 72 | ] 73 | 74 | for name, event in self.available_events.items(): 75 | thing['events'][name] = event['metadata'] 76 | thing['events'][name]['links'] = [ 77 | { 78 | 'rel': 'event', 79 | 'href': '{}/events/{}'.format(self.href_prefix, name), 80 | }, 81 | ] 82 | 83 | if self.ui_href is not None: 84 | thing['links'].append({ 85 | 'rel': 'alternate', 86 | 'mediaType': 'text/html', 87 | 'href': self.ui_href, 88 | }) 89 | 90 | if self.description: 91 | thing['description'] = self.description 92 | 93 | return thing 94 | 95 | def get_href(self): 96 | """Get this thing's href.""" 97 | if self.href_prefix: 98 | return self.href_prefix 99 | 100 | return '/' 101 | 102 | def get_ui_href(self): 103 | """Get the UI href.""" 104 | return self.ui_href 105 | 106 | def set_href_prefix(self, prefix): 107 | """ 108 | Set the prefix of any hrefs associated with this thing. 109 | 110 | prefix -- the prefix 111 | """ 112 | self.href_prefix = prefix 113 | 114 | for property_ in self.properties.values(): 115 | property_.set_href_prefix(prefix) 116 | 117 | for action_name in self.actions.keys(): 118 | for action in self.actions[action_name]: 119 | action.set_href_prefix(prefix) 120 | 121 | def set_ui_href(self, href): 122 | """ 123 | Set the href of this thing's custom UI. 124 | 125 | href -- the href 126 | """ 127 | self.ui_href = href 128 | 129 | def get_id(self): 130 | """ 131 | Get the ID of the thing. 132 | 133 | Returns the ID as a string. 134 | """ 135 | return self.id 136 | 137 | def get_title(self): 138 | """ 139 | Get the title of the thing. 140 | 141 | Returns the title as a string. 142 | """ 143 | return self.title 144 | 145 | def get_context(self): 146 | """ 147 | Get the type context of the thing. 148 | 149 | Returns the context as a string. 150 | """ 151 | return self.context 152 | 153 | def get_type(self): 154 | """ 155 | Get the type(s) of the thing. 156 | 157 | Returns the list of types. 158 | """ 159 | return self.type 160 | 161 | def get_description(self): 162 | """ 163 | Get the description of the thing. 164 | 165 | Returns the description as a string. 166 | """ 167 | return self.description 168 | 169 | def get_property_descriptions(self): 170 | """ 171 | Get the thing's properties as a dictionary. 172 | 173 | Returns the properties as a dictionary, i.e. name -> description. 174 | """ 175 | return {k: v.as_property_description() 176 | for k, v in self.properties.items()} 177 | 178 | def get_action_descriptions(self, action_name=None): 179 | """ 180 | Get the thing's actions as an array. 181 | 182 | action_name -- Optional action name to get descriptions for 183 | 184 | Returns the action descriptions. 185 | """ 186 | descriptions = [] 187 | 188 | if action_name is None: 189 | for name in self.actions: 190 | for action in self.actions[name]: 191 | descriptions.append(action.as_action_description()) 192 | elif action_name in self.actions: 193 | for action in self.actions[action_name]: 194 | descriptions.append(action.as_action_description()) 195 | 196 | return descriptions 197 | 198 | def get_event_descriptions(self, event_name=None): 199 | """ 200 | Get the thing's events as an array. 201 | 202 | event_name -- Optional event name to get descriptions for 203 | 204 | Returns the event descriptions. 205 | """ 206 | if event_name is None: 207 | return [e.as_event_description() for e in self.events] 208 | else: 209 | return [e.as_event_description() 210 | for e in self.events if e.get_name() == event_name] 211 | 212 | def add_property(self, property_): 213 | """ 214 | Add a property to this thing. 215 | 216 | property_ -- property to add 217 | """ 218 | property_.set_href_prefix(self.href_prefix) 219 | self.properties[property_.name] = property_ 220 | 221 | def remove_property(self, property_): 222 | """ 223 | Remove a property from this thing. 224 | 225 | property_ -- property to remove 226 | """ 227 | if property_.name in self.properties: 228 | del self.properties[property_.name] 229 | 230 | def find_property(self, property_name): 231 | """ 232 | Find a property by name. 233 | 234 | property_name -- the property to find 235 | 236 | Returns a Property object, if found, else None. 237 | """ 238 | return self.properties.get(property_name, None) 239 | 240 | def get_property(self, property_name): 241 | """ 242 | Get a property's value. 243 | 244 | property_name -- the property to get the value of 245 | 246 | Returns the properties value, if found, else None. 247 | """ 248 | prop = self.find_property(property_name) 249 | if prop: 250 | return prop.get_value() 251 | 252 | return None 253 | 254 | def get_properties(self): 255 | """ 256 | Get a mapping of all properties and their values. 257 | 258 | Returns a dictionary of property_name -> value. 259 | """ 260 | return {prop.get_name(): prop.get_value() 261 | for prop in self.properties.values()} 262 | 263 | def has_property(self, property_name): 264 | """ 265 | Determine whether or not this thing has a given property. 266 | 267 | property_name -- the property to look for 268 | 269 | Returns a boolean, indicating whether or not the thing has the 270 | property. 271 | """ 272 | return property_name in self.properties 273 | 274 | def set_property(self, property_name, value): 275 | """ 276 | Set a property value. 277 | 278 | property_name -- name of the property to set 279 | value -- value to set 280 | """ 281 | prop = self.find_property(property_name) 282 | if not prop: 283 | return 284 | 285 | prop.set_value(value) 286 | 287 | def get_action(self, action_name, action_id): 288 | """ 289 | Get an action. 290 | 291 | action_name -- name of the action 292 | action_id -- ID of the action 293 | 294 | Returns the requested action if found, else None. 295 | """ 296 | if action_name not in self.actions: 297 | return None 298 | 299 | for action in self.actions[action_name]: 300 | if action.id == action_id: 301 | return action 302 | 303 | return None 304 | 305 | def add_event(self, event): 306 | """ 307 | Add a new event and notify subscribers. 308 | 309 | event -- the event that occurred 310 | """ 311 | self.events.append(event) 312 | self.event_notify(event) 313 | 314 | def add_available_event(self, name, metadata): 315 | """ 316 | Add an available event. 317 | 318 | name -- name of the event 319 | metadata -- event metadata, i.e. type, description, etc., as a dict 320 | """ 321 | if metadata is None: 322 | metadata = {} 323 | 324 | self.available_events[name] = { 325 | 'metadata': metadata, 326 | 'subscribers': set(), 327 | } 328 | 329 | def perform_action(self, action_name, input_=None): 330 | """ 331 | Perform an action on the thing. 332 | 333 | action_name -- name of the action 334 | input_ -- any action inputs 335 | 336 | Returns the action that was created. 337 | """ 338 | if action_name not in self.available_actions: 339 | return None 340 | 341 | action_type = self.available_actions[action_name] 342 | 343 | # if 'input' in action_type['metadata']: 344 | # try: 345 | # validate(input_, action_type['metadata']['input']) 346 | # except ValidationError: 347 | # return None 348 | 349 | action = action_type['class'](self, input_=input_) 350 | action.set_href_prefix(self.href_prefix) 351 | self.action_notify(action) 352 | self.actions[action_name].append(action) 353 | return action 354 | 355 | def remove_action(self, action_name, action_id): 356 | """ 357 | Remove an existing action. 358 | 359 | action_name -- name of the action 360 | action_id -- ID of the action 361 | 362 | Returns a boolean indicating the presence of the action. 363 | """ 364 | action = self.get_action(action_name, action_id) 365 | if action is None: 366 | return False 367 | 368 | action.cancel() 369 | self.actions[action_name].remove(action) 370 | return True 371 | 372 | def add_available_action(self, name, metadata, cls): 373 | """ 374 | Add an available action. 375 | 376 | name -- name of the action 377 | metadata -- action metadata, i.e. type, description, etc., as a dict 378 | cls -- class to instantiate for this action 379 | """ 380 | if metadata is None: 381 | metadata = {} 382 | 383 | self.available_actions[name] = { 384 | 'metadata': metadata, 385 | 'class': cls, 386 | } 387 | self.actions[name] = [] 388 | 389 | def add_subscriber(self, ws): 390 | """ 391 | Add a new websocket subscriber. 392 | 393 | ws -- the websocket 394 | """ 395 | self.subscribers.add(ws) 396 | 397 | def remove_subscriber(self, ws): 398 | """ 399 | Remove a websocket subscriber. 400 | 401 | ws -- the websocket 402 | """ 403 | if ws in self.subscribers: 404 | self.subscribers.remove(ws) 405 | 406 | for name in self.available_events: 407 | self.remove_event_subscriber(name, ws) 408 | 409 | def add_event_subscriber(self, name, ws): 410 | """ 411 | Add a new websocket subscriber to an event. 412 | 413 | name -- name of the event 414 | ws -- the websocket 415 | """ 416 | print('add_event_subscriber:', name) 417 | if name in self.available_events: 418 | self.available_events[name]['subscribers'].add(ws) 419 | 420 | def remove_event_subscriber(self, name, ws): 421 | """ 422 | Remove a websocket subscriber from an event. 423 | 424 | name -- name of the event 425 | ws -- the websocket 426 | """ 427 | print('remove_event_subscriber:', name) 428 | if name in self.available_events and \ 429 | ws in self.available_events[name]['subscribers']: 430 | self.available_events[name]['subscribers'].remove(ws) 431 | 432 | def property_notify(self, property_): 433 | """ 434 | Notify all subscribers of a property change. 435 | 436 | property_ -- the property that changed 437 | """ 438 | message = json.dumps({ 439 | 'messageType': 'propertyStatus', 440 | 'data': { 441 | property_.name: property_.get_value(), 442 | } 443 | }) 444 | 445 | for subscriber in self.subscribers: 446 | subscriber.SendText(message) 447 | 448 | def action_notify(self, action): 449 | """ 450 | Notify all subscribers of an action status change. 451 | 452 | action -- the action whose status changed 453 | """ 454 | message = json.dumps({ 455 | 'messageType': 'actionStatus', 456 | 'data': action.as_action_description(), 457 | }) 458 | 459 | for subscriber in self.subscribers: 460 | subscriber.SendText(message) 461 | 462 | def event_notify(self, event): 463 | """ 464 | Notify all subscribers of an event. 465 | 466 | event -- the event that occurred 467 | """ 468 | if event.name not in self.available_events: 469 | return 470 | 471 | message = json.dumps({ 472 | 'messageType': 'event', 473 | 'data': event.as_event_description(), 474 | }) 475 | 476 | for subscriber in self.available_events[event.name]['subscribers']: 477 | subscriber.SendText(message) 478 | -------------------------------------------------------------------------------- /webthing/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions.""" 2 | 3 | import time 4 | import network 5 | 6 | 7 | def timestamp(): 8 | """ 9 | Get the current time. 10 | 11 | Returns the current time in the form YYYY-mm-ddTHH:MM:SS+00:00 12 | """ 13 | now = time.localtime() 14 | return '{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}+00:00'.format(*now[:6]) 15 | 16 | 17 | def get_addresses(): 18 | """ 19 | Get all IP addresses. 20 | 21 | Returns list of addresses. 22 | """ 23 | addresses = ['127.0.0.1'] 24 | 25 | station = network.WLAN(network.STA_IF) 26 | if station.isconnected(): 27 | addresses.append(station.ifconfig()[0]) 28 | 29 | return addresses 30 | -------------------------------------------------------------------------------- /webthing/value.py: -------------------------------------------------------------------------------- 1 | """An observable, settable value interface.""" 2 | 3 | from eventemitter import EventEmitter 4 | 5 | 6 | class Value(EventEmitter): 7 | """ 8 | A property value. 9 | 10 | This is used for communicating between the Thing representation and the 11 | actual physical thing implementation. 12 | 13 | Notifies all observers when the underlying value changes through an 14 | external update (command to turn the light off) or if the underlying sensor 15 | reports a new value. 16 | """ 17 | 18 | def __init__(self, initial_value, value_forwarder=None): 19 | """ 20 | Initialize the object. 21 | 22 | initial_value -- the initial value 23 | value_forwarder -- the method that updates the actual value on the 24 | thing 25 | """ 26 | EventEmitter.__init__(self) 27 | self.last_value = initial_value 28 | self.value_forwarder = value_forwarder 29 | 30 | def set(self, value): 31 | """ 32 | Set a new value for this thing. 33 | 34 | value -- value to set 35 | """ 36 | if self.value_forwarder is not None: 37 | self.value_forwarder(value) 38 | 39 | self.notify_of_external_update(value) 40 | 41 | def get(self): 42 | """Return the last known value from the underlying thing.""" 43 | return self.last_value 44 | 45 | def notify_of_external_update(self, value): 46 | """ 47 | Notify observers of a new value. 48 | 49 | value -- new value 50 | """ 51 | if value is not None and value != self.last_value: 52 | self.last_value = value 53 | self.emit('update', value) 54 | --------------------------------------------------------------------------------