├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── _config.yml ├── composer.json ├── examples ├── multiple-things.php └── single-thing.php ├── src └── WebThing │ ├── Action.php │ ├── ActionInterface.php │ ├── Event.php │ ├── EventInterface.php │ ├── MultipleThings.php │ ├── Property.php │ ├── PropertyInterface.php │ ├── Server │ ├── Handlers │ │ ├── ActionHandler.php │ │ ├── ActionIDHandler.php │ │ ├── ActionsHandler.php │ │ ├── BaseHandler.php │ │ ├── EventHandler.php │ │ ├── EventsHandler.php │ │ ├── PropertiesHandler.php │ │ ├── PropertyHandler.php │ │ ├── ThingHandler.php │ │ ├── ThingsHandler.php │ │ └── WebSocketThingHandler.php │ └── WebThingServer.php │ ├── SingleThing.php │ ├── Thing.php │ ├── ThingInterface.php │ ├── ThingsInterface.php │ └── Value.php └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.4' 4 | os: 5 | - linux 6 | addons: 7 | apt: 8 | packages: 9 | - python3-setuptools 10 | - python3-pip 11 | before_install: 12 | - chmod +x ./travis.sh 13 | script: 14 | - ./travis.sh 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM php:7.4-alpine 3 | 4 | LABEL maintainer='Malik Naik ' 5 | 6 | COPY . /app 7 | 8 | WORKDIR /app 9 | 10 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer 11 | 12 | RUN composer install 13 | 14 | EXPOSE 8888 15 | 16 | CMD ["php", "/app/examples/multiple-things.php"] 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web of Things 2 | 3 | [![travis](https://api.travis-ci.org/maliknaik16/webthing-php.svg?branch=master)](https://travis-ci.com/maliknaik16/webthing-php) 4 | [![GitHub forks](https://img.shields.io/github/forks/maliknaik16/webthing-php)](https://github.com/maliknaik16/webthing-php/network/) 5 | [![GitHub version](https://badge.fury.io/gh/maliknaik16%2Fwebthing-php.svg)](https://badge.fury.io/gh/maliknaik16%2Fwebthing-php) 6 | [![Source Code](https://img.shields.io/badge/source-maliknaik16%2Fwebthing--php-blue?style=flat-square)](https://github.com/maliknaik16/webthing-php) 7 | [![PHP Version](https://img.shields.io/badge/PHP-7.4%2B-orange)](https://php.net) 8 | [![Software License](https://img.shields.io/badge/license-MPL--2.0-green?style=flat-square)](https://github.com/maliknaik16/webthing-php/blob/master/LICENSE.txt) 9 | 10 | Implementation of an HTTP [Web Thing](https://iot.mozilla.org/wot/). This library is compatible with PHP 7.4+. 11 | 12 | # Installation 13 | 14 | The ``webthing`` can be installed using ``composer`` via the following command: 15 | 16 | ```bash 17 | composer require webthing/webthing:^0.0.1 18 | ``` 19 | 20 | # Running the Example 21 | The following list of commands clones this repository and installs all dependencies using the composer and runs the `single-thing.php` example. 22 | ```bash 23 | git clone https://github.com/maliknaik16/webthing-php.git 24 | cd webthing-php 25 | composer install 26 | php examples/single-thing.php 27 | ``` 28 | 29 | # Example Implementation 30 | 31 | In this code-walkthrough we will set up a dimmable light and a humidity sensor (both using fake data, of course). Both working examples can be found in [here](https://github.com/maliknaik16/webthing-php/tree/master/examples). 32 | 33 | ## Dimmable Light 34 | 35 | Imagine you have a dimmable light that you want to expose via the web of things API. The light can be turned on/off and the brightness can be set from 0% to 100%. Besides the name, description, and type, a [Light](https://iot.mozilla.org/schemas/#Light) is required to expose two properties: 36 | 37 | - ``on``: the state of the light, whether it is turned on or off 38 | 39 | - Setting this property via a ``PUT {"on": true/false}`` call to the REST API toggles the light. 40 | 41 | - ``brightness``: the brightness level of the light from 0-100% 42 | 43 | - Setting this property via a PUT call to the REST API sets the brightness level of this light. 44 | 45 | First we create a new Thing: 46 | 47 | ```php 48 | $light = new Thing( 49 | 'urn:dev:ops:my-lamp-1234', 50 | 'My Lamp', 51 | ['OnOffSwitch', 'Light'], 52 | 'A web connected lamp' 53 | ); 54 | ``` 55 | 56 | Now we can add the required properties. 57 | 58 | The ``on`` property reports and sets the on/off state of the light. For this, we need to have a ``Value`` object which holds the actual state and also a method to turn the light on/off. For our purposes, we just want to log the new state if the light is switched on/off. 59 | 60 | ```php 61 | $light->addProperty(new Property( 62 | $light, 63 | 'on', 64 | new Value(TRUE, function($v) { 65 | echo "On-State is now " . $v . "\n"; 66 | }), 67 | [ 68 | '@type' => 'OnOffProperty', 69 | 'title' => 'On/Off', 70 | 'type' => 'boolean', 71 | 'description' => 'Whether the lamp is turned on', 72 | ]) 73 | ); 74 | ``` 75 | 76 | The ``brightness`` property reports the brightness level of the light and sets the level. Like before, instead of actually setting the level of a light, we just log the level. 77 | 78 | ```php 79 | $light->addProperty(new Property( 80 | $light, 81 | 'brightness', 82 | new Value(50, function($v) { 83 | echo "Brightness is now " . $v . "\n"; 84 | }), 85 | [ 86 | '@type' => 'BrightnessProperty', 87 | 'title' => 'Brightness', 88 | 'type' => 'integer', 89 | 'description' => 'The level of light from 0-100', 90 | 'minimum' => 0, 91 | 'maximum' => 100, 92 | 'unit' => 'percent', 93 | ]) 94 | ); 95 | ``` 96 | 97 | Now we can add our newly created thing to the server and start it: 98 | 99 | ```php 100 | // If adding more than one thing, use MultipleThings() with a name. 101 | // In the single thing case, the thing's name will be broadcast. 102 | $server = new WebThingServer(new SingleThing($thing), '127.0.0.1', 8888, 8081); 103 | 104 | $server->start(); 105 | $server->startWebSocket(); 106 | ``` 107 | This will start the server, making the light available via the WoT REST API and announcing it as a discoverable resource on your local network via mDNS. 108 | 109 | ## Sensor 110 | 111 | Let's now also connect a humidity sensor to the server we set up for our light. 112 | 113 | A [MultiLevelSensor](https://iot.mozilla.org/schemas/#MultiLevelSensor) (a sensor that returns a level instead of just on/off) has one required property (besides the name, type, and optional description): ``level``. We want to monitor this property and get notified if the value changes. 114 | 115 | First we create a new Thing: 116 | 117 | ```php 118 | $sensor = new Thing( 119 | 'urn:dev:ops:my-humidity-sensor-1234', 120 | 'My Humidity Sensor', 121 | ['MultiLevelSensor'], 122 | 'A web connected humidity sensor' 123 | ); 124 | ``` 125 | 126 | Then we create and add the appropriate property: 127 | 128 | - ``level``: tells us what the sensor is actually reading 129 | 130 | - Contrary to the light, the value cannot be set via an API call, as it wouldn't make much sense, to SET what a sensor is reading. Therefore, we are creating a **readOnly** property. 131 | 132 | ```php 133 | $level = new Value(0.0); 134 | $sensor->addProperty(new Property( 135 | $sensor, 136 | 'level', 137 | $level, 138 | [ 139 | '@type' => 'LevelProperty', 140 | 'title' => 'Humidity', 141 | 'type' => 'number', 142 | 'description' => 'The current humidity in %', 143 | 'minimum' => 0, 144 | 'maximum' => 100, 145 | 'unit' => 'percent', 146 | 'readOnly' => TRUE, 147 | ]) 148 | ); 149 | ``` 150 | 151 | Now we have a sensor that constantly reports 0%. To make it usable, we need a thread or some kind of input when the sensor has a new reading available. For this purpose we start a thread that queries the physical sensor every few seconds. For our purposes, it just calls a fake method. 152 | 153 | ```php 154 | // $level is a `Value` object. 155 | // $loop is a `React\EventLoop\Factory` object. 156 | $loop->addPeriodicTimer(7, function() use ($level) { 157 | $new_level = readFromGpio(); 158 | printf("Setting new humidity level: %s\n", $new_level); 159 | $level->notifyOfExternalUpdate($new_level); 160 | }); 161 | 162 | 163 | function readFromGpio() { 164 | return abs(70.0 * rand() * (-0.5 + rand())); 165 | } 166 | ``` 167 | This will update our ``Value`` object with the sensor readings via the ``$level->notifyOfExternalUpdate(readFromGpio());`` call. The ``Value`` object now notifies the property and the thing that the value has changed, which in turn notifies all websocket listeners. 168 | 169 | # Resources 170 | - [https://iot.mozilla.org/wot](https://iot.mozilla.org/wot) 171 | - [https://iot.mozilla.org/framework/](https://iot.mozilla.org/framework/) 172 | - [https://iot.mozilla.org/gateway/](https://iot.mozilla.org/gateway/) 173 | - [https://www.w3.org/WoT/IG/](https://www.w3.org/WoT/IG/) 174 | 175 | # License 176 | 177 | Mozilla Public License Version 2.0 178 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webthing/webthing", 3 | "type": "library", 4 | "description": "A PHP Web Thing Server implementation.", 5 | "keywords": ["webthing", "php", "mozilla"], 6 | "license": "MPL-2.0", 7 | "homepage": "https://github.com/maliknaik16/webthing-php", 8 | "authors": [ 9 | { 10 | "name": "Malik Naik", 11 | "email": "maliknaik16@gmail.com", 12 | "homepage": "http://maliknaik.me/", 13 | "role": "Developer" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "WebThing\\": "src/" 19 | } 20 | }, 21 | "config": { 22 | "optimize-autoloader": true 23 | }, 24 | "require": { 25 | "php": ">=7.4", 26 | "cboden/ratchet": "^0.4.1", 27 | "ramsey/uuid": "^3.9.2", 28 | "react/http": "^0.8.5", 29 | "justinrainbow/json-schema": "^5.2.9", 30 | "myclabs/deep-copy": "^1.9.4", 31 | "malik/crimson": "^1.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/multiple-things.php: -------------------------------------------------------------------------------- 1 | toString(); 32 | }catch(UnsatisfiedDependencyException $e) { 33 | echo 'Caught exception: ' . $e->getMessage() . PHP_EOL; 34 | } 35 | 36 | parent::__construct($thing, $uuid, 'fade', $input); 37 | } 38 | 39 | public function performAction() { 40 | sleep($this->input['duration'] / 1000); 41 | $this->thing->setProperty('brightness', $this->input['brightness']); 42 | $this->thing->addEvent(new OverheatedEvent($this->thing, 102)); 43 | } 44 | } 45 | 46 | 47 | /** 48 | * A dimmable light that logs received commands to stdout. 49 | */ 50 | class ExampleDimmableLight extends Thing { 51 | public function __construct() { 52 | parent::__construct( 53 | 'urn:dev:ops:my-lamp-1234', 54 | 'My Lamp', 55 | ['OnOffSwitch', 'Light'], 56 | 'A web connected lamp' 57 | ); 58 | 59 | $this->addProperty(new Property( 60 | $this, 61 | 'on', 62 | new Value(TRUE, function($v) { 63 | echo "On-State is now " . $v . "\n"; 64 | }), 65 | [ 66 | '@type' => 'OnOffProperty', 67 | 'title' => 'On/Off', 68 | 'type' => 'boolean', 69 | 'description' => 'Whether the lamp is turned on', 70 | ]) 71 | ); 72 | 73 | $this->addProperty(new Property( 74 | $this, 75 | 'brightness', 76 | new Value(50, function($v) { 77 | echo "Brightness is now " . $v . "\n"; 78 | }), 79 | [ 80 | '@type' => 'BrightnessProperty', 81 | 'title' => 'Brightness', 82 | 'type' => 'integer', 83 | 'description' => 'The level of light from 0-100', 84 | 'minimum' => 0, 85 | 'maximum' => 100, 86 | 'unit' => 'percent', 87 | ]) 88 | ); 89 | 90 | $this->addAvailableAction( 91 | 'fade', 92 | [ 93 | 'title' => 'Fade', 94 | 'description' => 'Fade the lamp to a given level', 95 | 'input' => [ 96 | 'type' => 'object', 97 | 'required' => [ 98 | 'brightness', 99 | 'duration', 100 | ], 101 | 'properties' => [ 102 | 'brightness' => [ 103 | 'type' => 'integer', 104 | 'minimum' => 0, 105 | 'maximum' => 100, 106 | 'unit' => 'percent', 107 | ], 108 | 'duration' => [ 109 | 'type' => 'integer', 110 | 'minimum' => 1, 111 | 'unit' => 'milliseconds', 112 | ], 113 | ], 114 | ], 115 | ], 116 | 'FadeAction' 117 | ); 118 | 119 | $this->addAvailableEvent( 120 | 'overheated', 121 | [ 122 | 'description' => 'The lamp has exceeded its safe operating temperature', 123 | 'type' => 'number', 124 | 'unit' => 'degree celsius', 125 | ] 126 | ); 127 | } 128 | } 129 | 130 | class FakeGpioHumiditySensor extends Thing { 131 | 132 | private $level; 133 | 134 | public function __construct() { 135 | global $loop; 136 | 137 | parent::__construct( 138 | 'urn:dev:ops:my-humidity-sensor-1234', 139 | 'My Humidity Sensor', 140 | ['MultiLevelSensor'], 141 | 'A web connected humidity sensor' 142 | ); 143 | 144 | $this->level = new Value(0.0); 145 | $this->addProperty(new Property( 146 | $this, 147 | 'level', 148 | $this->level, 149 | [ 150 | '@type' => 'LevelProperty', 151 | 'title' => 'Humidity', 152 | 'type' => 'number', 153 | 'description' => 'The current humidity in %', 154 | 'minimum' => 0, 155 | 'maximum' => 100, 156 | 'unit' => 'percent', 157 | 'readOnly' => TRUE, 158 | ]) 159 | ); 160 | echo 'Starting the sensor update looping task' . PHP_EOL; 161 | 162 | $level = $this->level; 163 | $loop->addPeriodicTimer(7, function() use ($level) { 164 | $new_level = $this->readFromGpio(); 165 | printf("Setting new humidity level: %s\n", $new_level); 166 | $level->notifyOfExternalUpdate($new_level); 167 | }); 168 | } 169 | 170 | /** 171 | * Mimic an actual sensor updating its reading every couple seconds. 172 | */ 173 | public function readFromGpio() { 174 | return abs(70.0 * rand() * (-0.5 + rand())); 175 | } 176 | } 177 | 178 | function run_server() { 179 | global $loop; 180 | 181 | // Create a thing that represents a dimmable light 182 | $light = new ExampleDimmableLight(); 183 | 184 | // Create a thing that represents a humidity sensor 185 | $sensor = new FakeGpioHumiditySensor(); 186 | 187 | // If adding more than one thing, use MultipleThings() with a name. 188 | // In the single thing case, the thing's name will be broadcast. 189 | $server = new WebThingServer(new MultipleThings([$light, $sensor], 'LightAndTempDevice'), '127.0.0.1', 8888, 8081, [], NULL, '', $loop); 190 | 191 | // Start the server. 192 | $server->start(); 193 | $server->startWebSocket(); 194 | } 195 | 196 | run_server(); 197 | -------------------------------------------------------------------------------- /examples/single-thing.php: -------------------------------------------------------------------------------- 1 | toString(); 29 | }catch(UnsatisfiedDependencyException $e) { 30 | echo 'Caught exception: ' . $e->getMessage() . PHP_EOL; 31 | } 32 | 33 | parent::__construct($thing, $uuid, 'fade', $input); 34 | } 35 | 36 | public function performAction() { 37 | sleep($this->input['duration'] / 1000); 38 | $this->thing->setProperty('brightness', $this->input['brightness']); 39 | $this->thing->addEvent(new OverheatedEvent($this->thing, 102)); 40 | } 41 | } 42 | 43 | function make_thing() { 44 | $thing = new Thing( 45 | 'urn:dev:ops:my-lamp-1234', 46 | 'My Lamp', 47 | ['OnOffSwitch', 'Light'], 48 | 'A web connected lamp' 49 | ); 50 | 51 | $thing->addProperty(new Property( 52 | $thing, 53 | 'on', 54 | new Value(TRUE), 55 | [ 56 | '@type' => 'OnOffProperty', 57 | 'title' => 'On/Off', 58 | 'type' => 'boolean', 59 | 'description' => 'Whether the lamp is turned on', 60 | ]) 61 | ); 62 | 63 | $thing->addProperty(new Property( 64 | $thing, 65 | 'brightness', 66 | new Value(50), 67 | [ 68 | '@type' => 'BrightnessProperty', 69 | 'title' => 'Brightness', 70 | 'type' => 'integer', 71 | 'description' => 'The level of light from 0-100', 72 | 'minimum' => 0, 73 | 'maximum' => 100, 74 | 'unit' => 'percent', 75 | ]) 76 | ); 77 | 78 | $thing->addAvailableAction( 79 | 'fade', 80 | [ 81 | 'title' => 'Fade', 82 | 'description' => 'Fade the lamp to a given level', 83 | 'input' => [ 84 | 'type' => 'object', 85 | 'required' => [ 86 | 'brightness', 87 | 'duration', 88 | ], 89 | 'properties' => [ 90 | 'brightness' => [ 91 | 'type' => 'integer', 92 | 'minimum' => 0, 93 | 'maximum' => 100, 94 | 'unit' => 'percent', 95 | ], 96 | 'duration' => [ 97 | 'type' => 'integer', 98 | 'minimum' => 1, 99 | 'unit' => 'milliseconds', 100 | ], 101 | ], 102 | ], 103 | ], 104 | 'FadeAction' 105 | ); 106 | 107 | $thing->addAvailableEvent( 108 | 'overheated', 109 | [ 110 | 'description' => 'The lamp has exceeded its safe operating temperature', 111 | 'type' => 'number', 112 | 'unit' => 'degree celsius', 113 | ] 114 | ); 115 | 116 | return $thing; 117 | } 118 | 119 | 120 | function run_server() { 121 | $thing = make_thing(); 122 | 123 | $server = new WebThingServer(new SingleThing($thing), '127.0.0.1', 8888, 8081); 124 | 125 | $server->start(); 126 | $server->startWebSocket(); 127 | } 128 | 129 | 130 | run_server(); 131 | -------------------------------------------------------------------------------- /src/WebThing/Action.php: -------------------------------------------------------------------------------- 1 | id = $id; 83 | $this->thing = $thing; 84 | $this->name = $name; 85 | $this->input = $input; 86 | $this->href_prefix = ''; 87 | $this->href = sprintf("/actions/%s/%s", $this->name, $this->id); 88 | $this->status = 'created'; 89 | $this->time_requested = date("c", time()); 90 | $this->time_completed = NULL; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function asActionDescription() { 97 | $description = [ 98 | $this->name => [ 99 | 'href' => $this->href_prefix . $this->href, 100 | 'timeRequested' => $this->time_requested, 101 | 'status' => $this->status, 102 | ], 103 | ]; 104 | 105 | if(!empty($this->input)) { 106 | $description[$this->name]['input'] = $this->input; 107 | } 108 | 109 | if(!empty($this->time_completed)) { 110 | $description[$this->name]['timeCompleted'] = $this->time_completed; 111 | } 112 | 113 | return $description; 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function setHrefPrefix($prefix) { 120 | $this->href_prefix = $prefix; 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function getId() { 127 | return $this->id; 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function getName() { 134 | return $this->name; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function getHref() { 141 | return $this->href_prefix . $this->href; 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function getStatus() { 148 | return $this->status; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function getThing() { 155 | return $this->thing; 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function getTimeRequested() { 162 | return $this->time_requested; 163 | } 164 | 165 | /** 166 | * {@inheritdoc} 167 | */ 168 | public function getTimeCompleted() { 169 | return $this->time_completed; 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | */ 175 | public function getInput() { 176 | return $this->input; 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function start() { 183 | $this->status = 'pending'; 184 | $this->thing->actionNotify($this); 185 | $this->performAction(); 186 | $this->finish(); 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function performAction() { 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | public function cancel() { 199 | } 200 | 201 | /** 202 | * {@inheritdoc} 203 | */ 204 | public function finish() { 205 | $this->status = 'completed'; 206 | $this->time_completed = date("c", time()); 207 | $this->thing->actionNotify($this); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/WebThing/ActionInterface.php: -------------------------------------------------------------------------------- 1 | thing = $thing; 49 | $this->name = $name; 50 | $this->data = $data; 51 | $this->time = date("c", time()); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function asEventDescription() { 58 | $description = [ 59 | $this->name => [ 60 | 'timestamp' => $this->time, 61 | ], 62 | ]; 63 | 64 | if(!is_null($this->data)) { 65 | $description[$this->name]['data'] = $this->data; 66 | } 67 | 68 | return $description; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function getThing() { 75 | return $this->thing; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getName() { 82 | return $this->name; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function getData() { 89 | return $this->data; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getTime() { 96 | return $this->time; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/WebThing/EventInterface.php: -------------------------------------------------------------------------------- 1 | things = $things; 35 | $this->name = $name; 36 | } 37 | 38 | /** 39 | * Get the thing at the given index. 40 | * 41 | * @param string 42 | */ 43 | public function getThing($index) { 44 | $idx = intval($index); 45 | 46 | if($idx == 0 && !isset($this->things[$idx])) { 47 | return null; 48 | } 49 | 50 | if($idx < 0 || $idx >= count($this->things)) { 51 | return null; 52 | } 53 | 54 | return $this->things[$idx]; 55 | } 56 | 57 | /** 58 | * Get the list of things. 59 | */ 60 | public function getThings() { 61 | return $this->things; 62 | } 63 | 64 | /** 65 | * Get the mDNS server name. 66 | */ 67 | public function getName() { 68 | return $this->name; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/WebThing/Property.php: -------------------------------------------------------------------------------- 1 | thing = $thing; 65 | $this->name = $name; 66 | $this->value = $value; 67 | $this->href_prefix = ''; 68 | $this->href = sprintf("/properties/%s", $this->name); 69 | $this->metadata = isset($metadata) ? $metadata : []; 70 | 71 | $emitter = $this->value->getEventEmitter(); 72 | 73 | $emitter->on('valueUpdate', function($new_value) { 74 | $this->thing->propertyNotify($this); 75 | }); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function validateValue($value) { 82 | $validator = new Validator(); 83 | // TODO: CHECK OUT THIS LINE TO RAISE EXCEPTION 84 | if(isset($this->metadata['readOnly']) && $this->metadata['readOnly'] == true) { 85 | throw new \Exception("Read-only property '" . $this->name . "'."); 86 | } 87 | 88 | //var_dump($value); 89 | //$vvalue = $value->get(); 90 | //echo 'AFTER VALIDATE VALUE'; 91 | $validator->validate($value, $this->metadata); 92 | 93 | if(!$validator->isValid()) { 94 | throw new \Exception("Invalid property value for '" . $this->name . "'."); 95 | } 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function asPropertyDescription() { 102 | $copier = new DeepCopy(); 103 | 104 | $desc = $copier->copy($this->metadata); 105 | 106 | if(!array_key_exists('links', $desc)) { 107 | $desc['links'] = []; 108 | } 109 | 110 | $desc['links'][] = [ 111 | 'rel' => 'property', 112 | 'href' => $this->href_prefix . $this->href 113 | ]; 114 | 115 | return $desc; 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function setHrefPrefix($prefix) { 122 | $this->href_prefix = $prefix; 123 | } 124 | 125 | /** 126 | * {@inheritdoc} 127 | */ 128 | public function getHref() { 129 | return $this->href_prefix; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function getValue() { 136 | return $this->value->get(); 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public function setValue($value) { 143 | $this->validateValue($value); 144 | $this->value->set($value); 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function getName() { 151 | return $this->name; 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function getThing() { 158 | return $this->thing; 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | public function getMetadata() { 165 | return $this->metadata; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/WebThing/PropertyInterface.php: -------------------------------------------------------------------------------- 1 | '. 12 | */ 13 | class ActionHandler extends BaseHandler { 14 | 15 | /** 16 | * The thing from the ID of the URL path. 17 | * 18 | * @var WebThing\ThingInterface 19 | */ 20 | protected $thing; 21 | 22 | /** 23 | * Name of the action from the URL path. 24 | * 25 | * @var mixed 26 | */ 27 | protected $action_name; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function initialize() { 33 | parent::initialize(); 34 | $route_args = $this->getRouteArgs(); 35 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 36 | 37 | $this->thing = $this->getThing($thing_id); 38 | $this->action_name = array_key_exists('action_name', $route_args) ? $route_args['action_name'] : NULL; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function get() { 45 | 46 | if($this->thing == NULL) { 47 | $this->sendError(404); 48 | return; 49 | } 50 | 51 | $this->setStatus(200); 52 | $this->setContentType('application/json'); 53 | $this->write(json_encode($this->thing->getActionDescriptions($this->action_name))); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function post() { 60 | 61 | if($this->thing == NULL) { 62 | $this->sendError(404); 63 | return; 64 | } 65 | 66 | $message = json_decode($this->getRequest()->getBody()->getContents(), true); 67 | 68 | if($message === NULL && json_last_error() !== JSON_ERROR_NONE) { 69 | $this->setStatus(400); 70 | return; 71 | } 72 | 73 | $response = []; 74 | foreach($message as $name => $action_params) { 75 | if($name != $this->action_name) { 76 | continue; 77 | } 78 | 79 | $input = NULL; 80 | if(array_key_exists('input', $action_params)) { 81 | $input = $action_params['input']; 82 | } 83 | 84 | $action = $this->thing->performAction($name, $input); 85 | 86 | if($action) { 87 | $response = array_merge($response, $action->asActionDescription()); 88 | 89 | // Start the action 90 | // Implemented logic to run action in next iteration 91 | // TODO: Check whether it works as expected. 92 | $this->getLoop()->futureTick(function () use ($action) { 93 | $action->start(); 94 | }); 95 | } 96 | } 97 | 98 | $this->setStatus(201); 99 | $this->setContentType('application/json'); 100 | $this->write(json_encode($response)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/ActionIDHandler.php: -------------------------------------------------------------------------------- 1 | /'. 12 | */ 13 | class ActionIDHandler extends BaseHandler { 14 | 15 | /** 16 | * The thing from the ID of the URL path. 17 | * 18 | * @var WebThing\ThingInterface 19 | */ 20 | protected $thing; 21 | 22 | /** 23 | * Name of the action from the URL path. 24 | * 25 | * @var mixed 26 | */ 27 | protected $action_name; 28 | 29 | /** 30 | * The action ID from the URL path. 31 | * 32 | * @var mixed 33 | */ 34 | protected $action_id; 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function initialize() { 40 | parent::initialize(); 41 | $route_args = $this->getRouteArgs(); 42 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 43 | 44 | $this->thing = $this->getThing($thing_id); 45 | $this->action_name = array_key_exists('action_name', $route_args) ? $route_args['action_name'] : NULL; 46 | $this->action_id = array_key_exists('action_id', $route_args) ? $route_args['action_id'] : NULL; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function get() { 53 | 54 | if($this->thing == NULL) { 55 | $this->sendError(404); 56 | return; 57 | } 58 | 59 | $action = $this->thing->getAction($this->action_name, $this->action_id); 60 | 61 | if($action === NULL) { 62 | $this->setStatus(404); 63 | return; 64 | } 65 | 66 | $this->setStatus(200); 67 | $this->setContentType('application/json'); 68 | $this->write(json_encode($action->asActionDescription())); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function put() { 75 | // TODO: this is not yet defined in the spec 76 | 77 | if ($this->thing == NULL) { 78 | $this->sendError(404); 79 | return; 80 | } 81 | 82 | $this->setStatus(200); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function delete() { 89 | 90 | if ($this->thing == NULL || $this->action_name == NULL || $this->action_id == NULL) { 91 | $this->sendError(404); 92 | return; 93 | } 94 | 95 | if ($this->thing->removeAction($this->action_name, $this->action_id)) { 96 | $this->setStatus(204); 97 | } else { 98 | $this->setStatus(404); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/ActionsHandler.php: -------------------------------------------------------------------------------- 1 | getRouteArgs()) ? $this->getRouteArgs()['thing_id'] : '0'; 28 | $this->thing = $this->getThing($thing_id); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function get() { 35 | 36 | if($this->thing == NULL) { 37 | $this->sendError(404); 38 | return; 39 | } 40 | 41 | $this->setStatus(200); 42 | $this->setContentType('application/json'); 43 | $this->write(json_encode($this->thing->getActionDescriptions())); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function post() { 50 | 51 | if($this->thing == NULL) { 52 | $this->sendError(404); 53 | return; 54 | } 55 | 56 | try { 57 | $message = json_decode($this->getRequest()->getBody()->getContents(), true); 58 | } catch (\Exception $e) { 59 | $this->sendError(404); 60 | return; 61 | } 62 | 63 | $response = []; 64 | foreach($message as $action_name => $action_params) { 65 | $input = NULL; 66 | 67 | if(array_key_exists('input', $action_params)) { 68 | $input = $action_params['input']; 69 | } 70 | 71 | $action = $this->thing->performAction($action_name, $input); 72 | 73 | // TODO: RECHECK THIS IF CONDITION 74 | if($action) { 75 | $response = array_merge($response, $action->asActionDescription()); 76 | 77 | // Start the action 78 | // Implemented logic to run action in next iteration 79 | // TODO: Check whether it works as expected. 80 | $this->getLoop()->futureTick(function () use ($action) { 81 | $action->start(); 82 | }); 83 | } 84 | } 85 | 86 | $this->setStatus(201); 87 | $this->setContentType('application/json'); 88 | $this->write(json_encode($response)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/BaseHandler.php: -------------------------------------------------------------------------------- 1 | getClassArgs(); 36 | $this->things = $class_args['things']; 37 | $this->hosts = $class_args['hosts']; 38 | } 39 | 40 | /** 41 | * Validate Host header. 42 | */ 43 | public function prepare() { 44 | $host = $this->getRequest()->getHeaders()['Host']; 45 | if(!is_string($host)) { 46 | $host = $host[0]; 47 | } 48 | 49 | if($host !== NULL && in_array(strtolower($host), $this->hosts)) { 50 | return; 51 | } 52 | 53 | $this->sendError(403); 54 | } 55 | 56 | /** 57 | * Get the thing. 58 | * 59 | * @param string $thing_id 60 | * ID of the thing to get. 61 | * 62 | * @return mixed 63 | */ 64 | protected function getThing($thing_id) { 65 | return $this->things->getThing($thing_id); 66 | } 67 | 68 | /** 69 | * Set the default headers for all requests. 70 | */ 71 | public function setDefaultHeaders() { 72 | $this->setHeader('Access-Control-Allow-Origin', '*'); 73 | $this->setHeader('Access-Control-Allow-Headers', 74 | 'Origin, X-Requested-With, Content-Type, Accept'); 75 | $this->setHeader('Access-Control-Allow-Methods', 76 | 'GET, HEAD, PUT, POST, DELETE'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/EventHandler.php: -------------------------------------------------------------------------------- 1 | '. 12 | */ 13 | class EventHandler extends BaseHandler { 14 | 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function get() { 19 | $route_args = $this->getRouteArgs(); 20 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 21 | $event_name = array_key_exists('event_name', $route_args) ? $route_args['event_name'] : NULL; 22 | 23 | $thing = $this->getThing($thing_id); 24 | 25 | if($thing == NULL) { 26 | $this->sendError(404); 27 | return; 28 | } 29 | 30 | $this->setStatus(200); 31 | $this->setContentType('application/json'); 32 | $this->write(json_encode($thing->getEventDescriptions($event_name))); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/EventsHandler.php: -------------------------------------------------------------------------------- 1 | getRouteArgs(); 20 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 21 | 22 | $thing = $this->getThing($thing_id); 23 | 24 | if($thing == NULL) { 25 | $this->sendError(404); 26 | return; 27 | } 28 | 29 | $this->setStatus(200); 30 | $this->setContentType('application/json'); 31 | $this->write(json_encode($thing->getEventDescriptions())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/PropertiesHandler.php: -------------------------------------------------------------------------------- 1 | getRouteArgs(); 20 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 21 | 22 | $thing = $this->getThing($thing_id); 23 | 24 | if($thing === NULL) { 25 | $this->sendError(404); 26 | return; 27 | } 28 | 29 | $this->setStatus(200); 30 | $this->setContentType('application/json'); 31 | $this->write(json_encode($thing->getProperties())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/PropertyHandler.php: -------------------------------------------------------------------------------- 1 | '. 12 | */ 13 | class PropertyHandler extends BaseHandler { 14 | 15 | /** 16 | * The thing from the ID of the URL path. 17 | * 18 | * @var WebThing\ThingInterface 19 | */ 20 | protected $thing; 21 | 22 | /** 23 | * Name of the property from the URL path. 24 | * 25 | * @var mixed 26 | */ 27 | protected $property_name; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function initialize() { 33 | parent::initialize(); 34 | 35 | $route_args = $this->getRouteArgs(); 36 | $thing_id = array_key_exists('thing_id', $route_args) ? $route_args['thing_id'] : '0'; 37 | 38 | $this->thing = $this->getThing($thing_id); 39 | $this->property_name = array_key_exists('property_name', $route_args) ? $route_args['property_name'] : NULL; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function get() { 46 | 47 | if($this->thing == NULL) { 48 | $this->sendError(404); 49 | return; 50 | } 51 | 52 | if($this->thing->hasProperty($this->property_name)) { 53 | $this->setStatus(200); 54 | $this->setContentType('application/json'); 55 | $this->write(json_encode([ 56 | $this->property_name => $this->thing->getProperty($this->property_name), 57 | ])); 58 | }else{ 59 | $this->sendError(404); 60 | } 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function put() { 67 | 68 | if($this->thing == NULL) { 69 | $this->sendError(404); 70 | return; 71 | } 72 | 73 | $args = json_decode($this->getRequest()->getBody()->getContents(), true); 74 | 75 | if($args === NULL && json_last_error() !== JSON_ERROR_NONE) { 76 | $this->setStatus(400); 77 | return; 78 | } 79 | 80 | if(!array_key_exists($this->property_name, $args)) { 81 | $this->setStatus(400); 82 | return; 83 | } 84 | 85 | if($this->thing->hasProperty($this->property_name)) { 86 | try { 87 | $this->thing->setProperty($this->property_name, $args[$this->property_name]); 88 | } catch(\Exception $e) { 89 | $this->setStatus(400); 90 | return; 91 | } 92 | $this->setStatus(200); 93 | $this->setContentType('application/json'); 94 | $this->write(json_encode([ 95 | $this->property_name => $this->thing->getProperty($this->property_name), 96 | ])); 97 | 98 | } else { 99 | $this->setStatus(400); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/ThingHandler.php: -------------------------------------------------------------------------------- 1 | getClassArgs()['ws_port']) ? $this->getClassArgs()['ws_port'] : $this->getRequest()->getUri()->getPort(); 20 | $thing_id = array_key_exists('thing_id', $this->getRouteArgs()) ? $this->getRouteArgs()['thing_id'] : '0'; 21 | $thing = $this->getThing($thing_id); 22 | 23 | if($thing == NULL) { 24 | $this->sendError(404); 25 | return; 26 | } 27 | 28 | $description = $thing->asThingDescription(); 29 | $scheme = $this->getRequest()->getUri()->getScheme(); 30 | 31 | $host = $this->getRequest()->getHeaders()['Host']; 32 | if(!is_string($host)) { 33 | $host = $host[0]; 34 | } 35 | $host_arr = explode(':', $host); 36 | 37 | $ws_href = sprintf("%s://%s", $scheme == 'https' ? 'wss' : 'ws', $host_arr[0] . ':' . $ws_port); 38 | array_push($description['links'], [ 39 | 'rel' => 'alternate', 40 | 'href' => sprintf("%s%s", $ws_href, $thing->getHref()), 41 | ]); 42 | 43 | $description['base'] = sprintf("%s://%s%s", $scheme, $host, $thing->getHref()); 44 | 45 | $description['securityDefinitions'] = [ 46 | 'nosec_sc' => [ 47 | 'scheme' => 'nosec', 48 | ], 49 | ]; 50 | $description['security'] = 'nosec_sc'; 51 | 52 | $this->setStatus(200); 53 | $this->setContentType('application/json'); 54 | $this->write(json_encode($description)); 55 | $this->finish(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/ThingsHandler.php: -------------------------------------------------------------------------------- 1 | getClassArgs()['ws_port']) ? $this->getClassArgs()['ws_port'] : $this->getRequest()->getUri()->getPort(); 22 | $scheme = $this->getRequest()->getUri()->getScheme(); 23 | $ws = $scheme == 'https' ? 'wss' : 'ws'; 24 | $host = $this->getRequest()->getHeaders()['Host']; 25 | 26 | if(!is_string($host)) { 27 | $host = $host[0]; 28 | } 29 | $host_arr = explode(':', $host); 30 | $ws_href = sprintf("%s://%s", $ws, $host_arr[0] . ':' . $ws_port); 31 | 32 | $descriptions = []; 33 | foreach($this->things->getThings() as $thing_id => $thing) { 34 | $description = $thing->asThingDescription(); 35 | $description['href'] = $thing->getHref(); 36 | $description['links'][] = [ 37 | 'rel' => 'alternate', 38 | 'href' => sprintf("%s%s", $ws_href, $thing->getHref()), 39 | ]; 40 | $description['base'] = sprintf("%s://%s%s", $scheme, $host, $thing->getHref()); 41 | $description['securityDefinitions'] = [ 42 | 'nosec_sc' => [ 43 | 'scheme' => 'nosec', 44 | ], 45 | ]; 46 | $description['security'] = 'nosec_sc'; 47 | 48 | array_push($descriptions, $description); 49 | } 50 | 51 | $this->setStatus(200); 52 | $this->setContentType('application/json'); 53 | $this->write(json_encode($descriptions)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/WebThing/Server/Handlers/WebSocketThingHandler.php: -------------------------------------------------------------------------------- 1 | things = $things; 49 | $this->hosts = $hosts; 50 | $this->loop = $loop; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function onOpen(ConnectionInterface $conn) { 57 | parse_str($conn->httpRequest->getUri()->getQuery(), $requestQuery); 58 | $host = $conn->httpRequest->getHeaders()['Host']; 59 | 60 | $thing_id = array_key_exists('thing_id', $requestQuery) ? $requestQuery['thing_id'] : '0'; 61 | $this->thing = $this->getThing($thing_id); 62 | 63 | $not_found = new Response(404); 64 | 65 | if(!is_string($host)) { 66 | $host = $host[0]; 67 | } 68 | 69 | if($this->thing == NULL) { 70 | $conn->send(gPsr\str($not_found)); 71 | $conn->close(); 72 | return; 73 | } 74 | 75 | if(!is_null($host) && in_array($host, $this->hosts)) { 76 | $this->thing->addSubscriber($conn); 77 | return; 78 | } 79 | 80 | $response = new Response(403); 81 | $conn->send(gPsr\str($response)); 82 | $conn->close(); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function onMessage(ConnectionInterface $from, $msg) { 89 | $message = json_decode($msg, true); 90 | 91 | 92 | if(($message === NULL || empty($message)) && json_last_error() !== JSON_ERROR_NONE && $from !== NULL) { 93 | $from->send($this->jsonErrorResponse('400 Bad Request', 'Parsing request failed')); 94 | return; 95 | } 96 | 97 | if(!array_key_exists('messageType', $message) && !array_key_exists('data', $message)) { 98 | $from->send($this->jsonErrorResponse('400 Bad Request', 'Invalid message')); 99 | 100 | return; 101 | } 102 | 103 | 104 | $msgType = $message['messageType']; 105 | 106 | if($msgType == 'setProperty') { 107 | foreach($message['data'] as $property_name => $property_value) { 108 | try { 109 | $this->thing->setProperty($property_name, $property_value); 110 | } catch(\Exception $e) { 111 | $from->send($this->jsonErrorResponse('400 Bad Request', $e->getMessage())); 112 | } 113 | } 114 | }else if($msgType == 'requestAction') { 115 | foreach($message['data'] as $action_name => $action_params) { 116 | $input = NULL; 117 | 118 | if(array_key_exists('input', $action_params)) { 119 | $input = $action_params['input']; 120 | } 121 | 122 | $action = $this->thing->performAction($action_name, $input); 123 | 124 | // TODO: TEST THE FUTURE LOOP LATER 125 | if($action) { 126 | $this->loop->futureTick(function () use ($action) { 127 | $action->start(); 128 | }); 129 | }else{ 130 | $from->send($this->jsonErrorResponse('400 Bad Request', 'Invalid action request', TRUE, $message)); 131 | } 132 | } 133 | }else if($msgType == 'addEventSubscription') { 134 | foreach($message['data'] as $event_name => $event_) { 135 | $this->thing->addEventSubscriber($event_name, $from); 136 | } 137 | }else{ 138 | $from->send($this->jsonErrorResponse('400 Bad Request', 'Unknown messageType: ' . $msgType, TRUE, $message)); 139 | 140 | return; 141 | } 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function onClose(ConnectionInterface $conn) { 148 | $this->thing->removeSubscriber($conn); 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function onError(ConnectionInterface $conn, \Exception $e) { 155 | echo 'Caught Exception: ' . $e->getMessage(); 156 | } 157 | 158 | /** 159 | * Get the thing this request is for. 160 | */ 161 | public function getThing($thing_id) { 162 | return $this->things->getThing($thing_id); 163 | } 164 | 165 | /** 166 | * JSON Error message response 167 | */ 168 | private function jsonErrorResponse($statusMsg, $msg, $request = FALSE, $requestMsg = '') { 169 | $json_message = [ 170 | 'messageType' => 'error', 171 | 'data' => [ 172 | 'status' => $statusMsg, 173 | 'message' => $msg, 174 | ], 175 | ]; 176 | 177 | if($request) { 178 | $json_message['data']['request'] = $requestMsg; 179 | } 180 | 181 | return json_encode($json_message); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/WebThing/Server/WebThingServer.php: -------------------------------------------------------------------------------- 1 | things = $things; 107 | $this->name = $things->getName(); 108 | $this->hostname = $hostname; 109 | $this->httpPort = $httpPort; 110 | $this->wsPort = $wsPort; 111 | $this->additional_routes = $additional_routes; 112 | $this->base_path = rtrim($base_path, '/'); 113 | 114 | // TODO: Add more hosts 115 | $system_hostname = strtolower(gethostname()); 116 | 117 | $this->hosts = [ 118 | 'localhost', 119 | sprintf("localhost:%d", $this->httpPort), 120 | sprintf("localhost:%d", $this->wsPort), 121 | sprintf("%s.local", $system_hostname), 122 | sprintf("%s.local:%d", $system_hostname, $this->httpPort), 123 | sprintf("%s.local:%d", $system_hostname, $this->wsPort), 124 | ]; 125 | 126 | $addresses = gethostbynamel(gethostname()); 127 | 128 | foreach($addresses as $address) { 129 | $this->hosts[] = $address; 130 | $this->hosts[] = sprintf("%s:%d", $address, $this->httpPort); 131 | $this->hosts[] = sprintf("%s:%d", $address, $this->wsPort); 132 | } 133 | 134 | if(!is_null($this->hostname)) { 135 | $hname = strtolower($this->hostname); 136 | 137 | $this->hosts[] = $hname; 138 | $this->hosts[] = sprintf("%s:%d", $hname, $this->httpPort); 139 | $this->hosts[] = sprintf("%s:%d", $hname, $this->wsPort); 140 | } 141 | 142 | if($this->things instanceof MultipleThings) { 143 | foreach(array_values($this->things->getThings()) as $i => $thing) { 144 | $thing->setHrefPrefix(sprintf("%s/%s", $this->base_path, $i)); 145 | } 146 | $this->multipleThings(); 147 | } elseif($this->things instanceof SingleThing) { 148 | $this->things->getThing()->setHrefPrefix($this->base_path); 149 | $this->singleThing(); 150 | } 151 | 152 | $app = new App($this->handlers); 153 | 154 | if(!empty($additional_routes)) { 155 | $app->addHandlers($additional_routes); 156 | } 157 | 158 | if($loop == NULL) { 159 | $eventLoop = Factory::create(); 160 | }else{ 161 | $eventLoop = $loop; 162 | } 163 | $this->server = new HttpServer($app, $tls_options, $hostname, $httpPort, $eventLoop); 164 | $this->webSocketApp = new Application('localhost', $wsPort, $hostname, $eventLoop); 165 | $thingHandler = new WebSocketThingHandler($this->things, $this->hosts, $eventLoop); 166 | $this->webSocketApp->route('/', $thingHandler, array('*')); 167 | $this->webSocketApp->route('/{thing_id}', $thingHandler, array('*')); 168 | } 169 | 170 | /** 171 | * Handle the requests for multiple things. 172 | */ 173 | public function multipleThings() { 174 | $class_args = [ 175 | 'things' => $this->things, 176 | 'hosts' => $this->hosts 177 | ]; 178 | 179 | $class_args_ws = [ 180 | 'things' => $this->things, 181 | 'hosts' => $this->hosts, 182 | 'ws_port' => $this->wsPort, 183 | ]; 184 | 185 | $this->handlers = [ 186 | [ 187 | '\/?', 188 | 'WebThing\Server\Handlers\ThingsHandler', 189 | $class_args_ws, 190 | ], 191 | [ 192 | '\/(?P\d+)\/?', 193 | 'WebThing\Server\Handlers\ThingHandler', 194 | $class_args_ws, 195 | ], 196 | [ 197 | '\/(?P\d+)\/properties\/?', 198 | 'WebThing\Server\Handlers\PropertiesHandler', 199 | $class_args, 200 | ], 201 | [ 202 | '\/(?P\d+)\/properties\/' . 203 | '(?P[^\/]+)\/?', 204 | 'WebThing\Server\Handlers\PropertyHandler', 205 | $class_args, 206 | ], 207 | [ 208 | '\/(?P\d+)\/actions\/?', 209 | 'WebThing\Server\Handlers\ActionsHandler', 210 | $class_args, 211 | ], 212 | [ 213 | '\/(?P\d+)\/actions\/(?P[^\/]+)\/?', 214 | 'WebThing\Server\Handlers\ActionHandler', 215 | $class_args, 216 | ], 217 | [ 218 | '\/(?P\d+)\/actions\/' . 219 | '(?P[^\/]+)\/(?P[^\/]+)\/?', 220 | 'WebThing\Server\Handlers\ActionIDHandler', 221 | $class_args, 222 | ], 223 | [ 224 | '\/(?P\d+)\/events\/?', 225 | 'WebThing\Server\Handlers\EventsHandler', 226 | $class_args, 227 | ], 228 | [ 229 | '\/(?P\d+)\/events\/(?P[^\/]+)\/?', 230 | 'WebThing\Server\Handlers\EventHandler', 231 | $class_args, 232 | ], 233 | ]; 234 | } 235 | 236 | /** 237 | * Handle the requests for single thing. 238 | */ 239 | public function singleThing() { 240 | 241 | $class_args = [ 242 | 'things' => $this->things, 243 | 'hosts' => $this->hosts 244 | ]; 245 | 246 | $class_args_ws = [ 247 | 'things' => $this->things, 248 | 'hosts' => $this->hosts, 249 | 'ws_port' => $this->wsPort, 250 | ]; 251 | 252 | $this->handlers = [ 253 | [ 254 | '\/?', 255 | 'WebThing\Server\Handlers\ThingHandler', 256 | $class_args_ws, 257 | ], 258 | [ 259 | '\/properties\/?', 260 | 'WebThing\Server\Handlers\PropertiesHandler', 261 | $class_args, 262 | ], 263 | [ 264 | '\/properties\/' . 265 | '(?P[^\/]+)\/?', 266 | 'WebThing\Server\Handlers\PropertyHandler', 267 | $class_args, 268 | ], 269 | [ 270 | '\/actions\/?', 271 | 'WebThing\Server\Handlers\ActionsHandler', 272 | $class_args, 273 | ], 274 | [ 275 | '\/actions\/(?P[^\/]+)\/?', 276 | 'WebThing\Server\Handlers\ActionHandler', 277 | $class_args, 278 | ], 279 | [ 280 | '\/actions\/' . 281 | '(?P[^\/]+)\/(?P[^\/]+)\/?', 282 | 'WebThing\Server\Handlers\ActionIDHandler', 283 | $class_args, 284 | ], 285 | [ 286 | '\/events\/?', 287 | 'WebThing\Server\Handlers\EventsHandler', 288 | $class_args, 289 | ], 290 | [ 291 | '\/events\/(?P[^\/]+)\/?', 292 | 'WebThing\Server\Handlers\EventHandler', 293 | $class_args, 294 | ], 295 | ]; 296 | } 297 | 298 | public function start() { 299 | $this->server->start(); 300 | } 301 | 302 | public function startWebSocket() { 303 | $this->webSocketApp->run(); 304 | } 305 | 306 | public function stop() { 307 | $this->server->stop(); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/WebThing/SingleThing.php: -------------------------------------------------------------------------------- 1 | thing = $thing; 27 | } 28 | 29 | /** 30 | * Get the thing. 31 | */ 32 | public function getThing($index = '') { 33 | return $this->thing; 34 | } 35 | 36 | /** 37 | * Get the list of things. 38 | */ 39 | public function getThings() { 40 | return [ 41 | $this->thing, 42 | ]; 43 | } 44 | 45 | /** 46 | * Get the mDNS server name. 47 | */ 48 | public function getName() { 49 | return $this->thing->getTitle(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/WebThing/Thing.php: -------------------------------------------------------------------------------- 1 | id = $id; 120 | $this->type = $type; 121 | $this->title = $title; 122 | $this->description = $description; 123 | $this->context = 'https://iot.mozilla.org/schemas'; 124 | $this->properties = []; 125 | $this->available_actions = []; 126 | $this->available_events = []; 127 | $this->actions = []; 128 | $this->events = []; 129 | $this->subscribers = new \SplObjectStorage; 130 | $this->href_prefix = ''; 131 | $this->ui_href = null; 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function asThingDescription() { 138 | $this->thing = [ 139 | 'id' => $this->id, 140 | 'title' => $this->title, 141 | '@context' => $this->context, 142 | 'properties' => $this->getPropertyDescriptions(), 143 | 'actions' => [], 144 | 'events' => [], 145 | 'links' => [ 146 | [ 147 | 'rel' => 'properties', 148 | 'href' => sprintf("%s/properties", $this->href_prefix), 149 | ], 150 | [ 151 | 'rel' => 'actions', 152 | 'href' => sprintf("%s/actions", $this->href_prefix), 153 | ], 154 | [ 155 | 'rel' => 'events', 156 | 'href' => sprintf("%s/events", $this->href_prefix), 157 | ], 158 | ], 159 | ]; 160 | 161 | foreach($this->available_actions as $name => $action) { 162 | $this->thing['actions'][$name] = $action['metadata']; 163 | $this->thing['actions'][$name]['links'] = [ 164 | [ 165 | 'rel' => 'action', 166 | 'href' => sprintf("%s/actions/%s", $this->href_prefix, $name), 167 | ], 168 | ]; 169 | } 170 | 171 | foreach($this->available_events as $name => $event) { 172 | $this->thing['events'][$name] = $event['metadata']; 173 | $this->thing['events'][$name]['links'] = [ 174 | [ 175 | 'rel' => 'event', 176 | 'href' => sprintf("%s/events/%s", $this->href_prefix, $name), 177 | ] 178 | ]; 179 | } 180 | 181 | if($this->ui_href != null) { 182 | $this->thing['links'][] = [ 183 | 'rel' => 'alternate', 184 | 'mediaType' => 'text/html', 185 | 'href' => $this->ui_href, 186 | ]; 187 | } 188 | 189 | if(!empty($this->description)) { 190 | $this->thing['description'] = $this->description; 191 | } 192 | 193 | if(!empty($this->type)) { 194 | $this->thing['@type'] = $this->type; 195 | } 196 | 197 | return $this->thing; 198 | } 199 | 200 | /** 201 | * {@inheritdoc} 202 | */ 203 | public function getPropertyDescriptions() { 204 | $property_descriptions = []; 205 | 206 | foreach($this->properties as $k => $v) { 207 | $property_descriptions[$k] = $v->asPropertyDescription(); 208 | } 209 | 210 | return $property_descriptions; 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function getHref() { 217 | if(empty($this->href_prefix)) { 218 | return '/'; 219 | } 220 | 221 | return $this->href_prefix; 222 | } 223 | 224 | /** 225 | * {@inheritdoc} 226 | */ 227 | public function getUiHref() { 228 | return $this->ui_href; 229 | } 230 | 231 | /** 232 | * {@inheritdoc} 233 | */ 234 | public function propertyNotify(PropertyInterface $property) { 235 | $message = json_encode([ 236 | 'messageType' => 'propertyStatus', 237 | 'data' => [ 238 | $property->getName() => $property->getValue() 239 | ] 240 | ]); 241 | 242 | // TODO: SEND TO THE WEBSOCKET 243 | //$this->webSocketHandler->notifyChange($message); 244 | 245 | foreach($this->subscribers as $subscriber) { 246 | $subscriber->send($message); 247 | } 248 | } 249 | 250 | /** 251 | * {@inheritdoc} 252 | */ 253 | public function setHrefPrefix($prefix) { 254 | $this->href_prefix = $prefix; 255 | 256 | foreach($this->properties as $property) { 257 | $property->setHrefPrefix($prefix); 258 | } 259 | 260 | // TODO: CONFIRM THIS IMPLEMENTATION 261 | foreach($this->actions as $action_name => $value) { 262 | foreach($this->actions[$action_name] as $action) { 263 | $action->setHrefPrefix($prefix); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * {@inheritdoc} 270 | */ 271 | public function setUiHref($href) { 272 | $this->ui_href = $href; 273 | } 274 | 275 | /** 276 | * {@inheritdoc} 277 | */ 278 | public function getId() { 279 | return $this->id; 280 | } 281 | 282 | /** 283 | * {@inheritdoc} 284 | */ 285 | public function getTitle() { 286 | return $this->title; 287 | } 288 | 289 | /** 290 | * {@inheritdoc} 291 | */ 292 | public function getContext() { 293 | return $this->context; 294 | } 295 | 296 | /** 297 | * {@inheritdoc} 298 | */ 299 | public function getType() { 300 | return $this->type; 301 | } 302 | 303 | /** 304 | * {@inheritdoc} 305 | */ 306 | public function getDescription() { 307 | return $this->description; 308 | } 309 | 310 | /** 311 | * {@inheritdoc} 312 | */ 313 | public function getActionDescriptions($action_name = '') { 314 | $descriptions = []; 315 | 316 | if(empty($action_name)) { 317 | // TODO: Figure out the action structure 318 | foreach($this->actions as $name => $value) { 319 | foreach($this->actions[$name] as $action) { 320 | array_push($descriptions, $action->asActionDescription()); 321 | } 322 | } 323 | }else if(array_key_exists($action_name, $this->actions)) { 324 | foreach($this->actions[$action_name] as $action) { 325 | array_push($descriptions, $action->asActionDescription()); 326 | } 327 | } 328 | return $descriptions; 329 | } 330 | 331 | /** 332 | * {@inheritdoc} 333 | */ 334 | public function getEventDescriptions($event_name = '') { 335 | $descriptions = []; 336 | if(empty($event_name)) { 337 | foreach($this->events as $event) { 338 | array_push($descriptions, $event->asEventDescription()); 339 | //$descriptions[] = $event->asEventDescription(); 340 | } 341 | }else{ 342 | foreach($this->events as $event) { 343 | if($event->getName() == $event_name) { 344 | array_push($descriptions, $event->asEventDescription()); 345 | //$descriptions[] = $event->asEventDescription(); 346 | } 347 | } 348 | } 349 | 350 | return $descriptions; 351 | } 352 | 353 | /** 354 | * {@inheritdoc} 355 | */ 356 | public function addProperty(PropertyInterface $property) { 357 | $property->setHrefPrefix($this->href_prefix); 358 | $this->properties[$property->getName()] = $property; 359 | } 360 | 361 | /** 362 | * {@inheritdoc} 363 | */ 364 | public function removeProperty(PropertyInterface $property) { 365 | if(in_array($property->getName(), $this->properties)) { 366 | unset($this->properties[$property->getName()]); 367 | } 368 | } 369 | 370 | /** 371 | * {@inheritdoc} 372 | */ 373 | public function findProperty($property_name) { 374 | if(isset($this->properties[$property_name])) { 375 | return $this->properties[$property_name]; 376 | } 377 | 378 | return null; 379 | } 380 | 381 | /** 382 | * {@inheritdoc} 383 | */ 384 | public function getProperty($property_name) { 385 | $property = $this->findProperty($property_name); 386 | 387 | if(!is_null($property)) { 388 | return $property->getValue(); 389 | } 390 | 391 | return null; 392 | } 393 | 394 | /** 395 | * {@inheritdoc} 396 | */ 397 | public function getProperties() { 398 | 399 | $properties = []; 400 | 401 | foreach($this->properties as $property) { 402 | $properties = array_merge($properties, [ 403 | $property->getName() => $property->getValue() 404 | ]); 405 | } 406 | 407 | return $properties; 408 | } 409 | 410 | /** 411 | * {@inheritdoc} 412 | */ 413 | public function hasProperty($property_name) { 414 | return array_key_exists($property_name, $this->properties); 415 | } 416 | 417 | /** 418 | * {@inheritdoc} 419 | */ 420 | public function setProperty($property_name, $value) { 421 | $property = $this->findProperty($property_name); 422 | 423 | if(!$property) { 424 | return; 425 | } 426 | 427 | $property->setValue($value); 428 | } 429 | 430 | /** 431 | * {@inheritdoc} 432 | */ 433 | public function getAction($action_name, $action_id) { 434 | if(!array_key_exists($action_name, $this->actions)) { 435 | return NULL; 436 | } 437 | 438 | foreach($this->actions[$action_name] as $k => $action) { 439 | if($action->getId() == $action_id) { 440 | return $action; 441 | } 442 | } 443 | 444 | return NULL; 445 | } 446 | 447 | /** 448 | * {@inheritdoc} 449 | */ 450 | public function addEvent(EventInterface $event) { 451 | $this->events[] = $event; 452 | $this->eventNotify($event); 453 | } 454 | 455 | /** 456 | * {@inheritdoc} 457 | */ 458 | public function addAvailableEvent($name, $metadata) { 459 | if(!$metadata) { 460 | $metadata = []; 461 | } 462 | 463 | $this->available_events[$name] = [ 464 | 'metadata' => $metadata, 465 | 'subscribers' => new \SplObjectStorage, 466 | ]; 467 | } 468 | 469 | /** 470 | * {@inheritdoc} 471 | */ 472 | public function performAction($action_name, $input = NULL) { 473 | 474 | // TODO: Re check this class again. 475 | if(!array_key_exists($action_name, $this->available_actions)) { 476 | return NULL; 477 | } 478 | 479 | $action_type = $this->available_actions[$action_name]; 480 | 481 | if(array_key_exists('input', $action_type['metadata'])) { 482 | $validator = new Validator(); 483 | $data = json_decode(json_encode($input)); 484 | $validator->validate($data, $action_type['metadata']['input']); 485 | 486 | if(!$validator->isValid()) { 487 | return NULL; 488 | } 489 | } 490 | 491 | $class_name = $action_type['class']; 492 | $action = new $class_name($this, $input); 493 | $action->setHrefPrefix($this->href_prefix); 494 | $this->actionNotify($action); 495 | array_push($this->actions[$action_name], $action); 496 | 497 | return $action; 498 | } 499 | 500 | /** 501 | * {@inheritdoc} 502 | */ 503 | public function removeAction($action_name, $action_id) { 504 | $action = $this->getAction($action_name, $action_id); 505 | 506 | if(!$action) { 507 | return FALSE; 508 | } 509 | 510 | $action->cancel(); 511 | // TODO: Find the solution to remove specific element from the array. 512 | 513 | if(($key = array_search($action, $this->actions[$action_name], TRUE)) !== FALSE) { 514 | unset($this->actions[$action_name][$key]); 515 | return TRUE; 516 | } 517 | return FALSE; 518 | } 519 | 520 | /** 521 | * {@inheritdoc} 522 | */ 523 | public function addAvailableAction($name, $metadata, $cls) { 524 | if(!$metadata) { 525 | $metadata = []; 526 | } 527 | 528 | $this->available_actions[$name] = [ 529 | 'metadata' => $metadata, 530 | 'class' => $cls, 531 | ]; 532 | 533 | $this->actions[$name] = []; 534 | } 535 | 536 | /** 537 | * {@inheritdoc} 538 | */ 539 | public function addSubscriber($ws) { 540 | $this->subscribers->attach($ws); 541 | } 542 | 543 | /** 544 | * {@inheritdoc} 545 | */ 546 | public function removeSubscriber($ws) { 547 | if($this->subscribers->contains($ws)) { 548 | $this->subscribers->detach($ws); 549 | } 550 | 551 | foreach($this->available_events as $name => $value_) { 552 | $this->removeEventSubscriber($name, $ws); 553 | } 554 | } 555 | 556 | /** 557 | * {@inheritdoc} 558 | */ 559 | public function addEventSubscriber($name, $ws) { 560 | if(array_key_exists($name, $this->available_events)) { 561 | if(array_key_exists('subscribers', $this->available_events[$name])) { 562 | $this->available_events[$name]['subscribers']->attach($ws); 563 | }else{ 564 | $this->available_events[$name]['subscribers'] = new \SplObjectStorage; 565 | $this->available_events[$name]['subscribers']->attach($ws); 566 | } 567 | } 568 | } 569 | 570 | /** 571 | * {@inheritdoc} 572 | */ 573 | public function removeEventSubscriber($name, $ws) { 574 | if(array_key_exists($name, $this->available_events) && 575 | $this->available_events[$name]['subscribers']->contains($ws) ) { 576 | $this->available_events[$name]['subscribers']->detach($ws); 577 | //($key = array_search($ws, $this->available_events[$name]['subscribers'])) !== FALSE) { 578 | //unset($this->available_events[$name]['subscribers'][$key]); 579 | } 580 | } 581 | 582 | /** 583 | * {@inheritdoc} 584 | */ 585 | public function actionNotify(ActionInterface $action) { 586 | $message = json_encode([ 587 | 'messageType' => 'actionStatus', 588 | 'data' => $action->asActionDescription(), 589 | ]); 590 | 591 | // TODO: RECHECK THE FOLLOWING 592 | foreach($this->subscribers as $subscriber) { 593 | $subscriber->send($message); 594 | } 595 | } 596 | 597 | /** 598 | * {@inheritdoc} 599 | */ 600 | public function eventNotify(EventInterface $event) { 601 | if(!array_key_exists($event->getName(), $this->available_events)) { 602 | return; 603 | } 604 | 605 | $message = json_encode([ 606 | 'messageType' => 'event', 607 | 'data' => $event->asEventDescription(), 608 | ]); 609 | 610 | foreach($this->available_events[$event->getName()]['subscribers'] as $subscriber) { 611 | $subscriber->send($message); 612 | } 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /src/WebThing/ThingInterface.php: -------------------------------------------------------------------------------- 1 | emitter = new EventEmitter(); 43 | $this->last_value = $initial_value; 44 | $this->value_forwarder = $value_forwarder; 45 | } 46 | 47 | /** 48 | * Set the new value for this property. 49 | */ 50 | public function set($value) { 51 | if($this->value_forwarder != null) { 52 | $value_forwarder = $this->value_forwarder; 53 | $value_forwarder($value); 54 | } 55 | 56 | $this->notifyOfExternalUpdate($value); 57 | } 58 | 59 | /** 60 | * Return the last known value from the thing. 61 | */ 62 | public function get() { 63 | return $this->last_value; 64 | } 65 | 66 | /** 67 | * Notify observers of a new value. 68 | */ 69 | public function notifyOfExternalUpdate($new_value) { 70 | if($new_value != null && $new_value != $this->last_value) { 71 | $this->last_value = $new_value; 72 | $this->emitter->emit('valueUpdate', [$new_value]); 73 | } 74 | } 75 | 76 | /** 77 | * Returns the Event emitter. 78 | */ 79 | public function getEventEmitter() { 80 | return $this->emitter; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | git clone https://github.com/maliknaik16/webthing-php.git 4 | cd webthing-php 5 | 6 | # clone the webthing-tester 7 | git clone https://github.com/mozilla-iot/webthing-tester 8 | pip3 install --user -r webthing-tester/requirements.txt 9 | 10 | composer install 11 | 12 | export PHP_PATH=. 13 | # build and test the single-thing example 14 | php examples/single-thing.php & 15 | EXAMPLE_PID=$! 16 | sleep 5 17 | ./webthing-tester/test-client.py --protocol http --host localhost --port 8888 --debug 18 | kill -15 $EXAMPLE_PID 19 | 20 | # build and test the multiple-things example 21 | php examples/multiple-things.php & 22 | EXAMPLE_PID=$! 23 | sleep 5 24 | ./webthing-tester/test-client.py --protocol http --host localhost --port 8888 --path-prefix "/0" --debug 25 | kill -15 $EXAMPLE_PID 26 | --------------------------------------------------------------------------------