GithubHelp home page GithubHelp logo

andersonshatch / soma-ctrl Goto Github PK

View Code? Open in Web Editor NEW
21.0 4.0 6.0 440 KB

Node util for controlling SOMA smart shade via MQTT or HTTP

License: MIT License

JavaScript 100.00%
soma btle bluetooth-le bluetooth blind home-automation nodejs home-assistant mqtt http

soma-ctrl's Introduction

SOMA Blind Controller Util npm version

Util for controlling SOMA smart shade, either over MQTT or via a HTTP API

Requirements

  • SOMA smart shade device that has been configured with the SOMA app
  • Bluetooth 4.0 LE hardware
  • OS supported by noble (I've only tested on macOS and Raspbian)
  • Node 8.16.1 or higher (Tested on Node 12.10.0, 11.15.0, 10.15.2, 9.11.2, 8.16.1)
  • (Potentially a Bluetooth stick to itself, if you're using some other Bluetooth software, see #59)

Installation

Run npm install -g soma-ctrl

Usage

somactrl by itself will print usage information

By default, device scanning will be active for 30 seconds, and any supported ones will be connected to. You may optionally:

  • Alter the device scanning timeout, e.g. scan for 60 seconds: somactrl -t 60
  • Specify the number of devices you expect to connect to, e.g. 4 devices expected: somactrl -e 4
  • Manually specify a list of device IDs to connect to, e.g. connect to RISE108 and RISE117: somactrl RISE108 RISE117
  • Manually specify a list of MAC addresses to connect to, e.g.: somactrl f5:11:7b:ee:f3:43

You must then specify options to use either MQTT, HTTP or both

To use with HTTP

Specify a port for the API to listen on with -l: somactrl -l 3000

To use with MQTT

Specify a broker URL with --url option: somactrl --url mqtt://yourbroker (mqtt/mqtts/ws/wss accepted)

Username and password for MQTT may be specified with -u and -p option

If no password argument is supplied, you can enter it interactively

Base topic defaults to homeassistant, but may be configured with the -topic option

MQTT

For each device connected, data will be published to the following topics: <baseTopic>/cover/<deviceID>/position - position

<baseTopic>/cover/<deviceID>/connected - connected or disconnected

<baseTopic>/cover/<deviceID>/battery - battery level

To issue commands: Move: <baseTopic>/cover/<deviceID>/move - message: int position between 0 (closed) and 100 (open)

Stop: <baseTopic>/cover/<deviceID>/move - message: 'stop'

Identify (beep device): <baseTopic>/cover/<deviceID/identify - message is ignored

In addition, for use with Home Assistant MQTT Discovery:

To automatically setup the cover device: <baseTopic>/cover/<deviceID>/config will be set to, e.g.:

{
    "name": "Living Room1",
    "availability_topic": "homeassistant/cover/RISE148/connection",
    "payload_available": "connected",
    "payload_not_available": "disconnected",
    "position_topic": "homeassistant/cover/RISE148/position",
    "set_position_topic": "homeassistant/cover/RISE148/move",
    "command_topic": "homeassistant/cover/RISE148/move",
    "payload_open": 100,
    "payload_close": 0,
    "unique_id": "soma_RISE148_cover",
    "device": {
        "identifiers": "soma_RISE148",
        "name": "RISE148",
        "manufacturer": "Soma",
        "model": "Smart Shade"
    }
}

To automatically setup a battery sensor: <baseTopic>/sensor/cover_<deviceName|slugified>_battery/config will be set to, e.g.:

{
    "name": "Cover <deviceID> battery",
    "state_topic": "<baseTopic>/cover/<deviceID>/battery",
    "unit_of_measurement": "%",
    "device_class": "battery",
    "unique_id": "soma_<deviceID>_battery",
    "device": {
        "identifiers": "soma_RISE148",
        "name": "RISE148",
        "manufacturer": "Soma",
        "model": "Smart Shade"
    }
}

Parameters

<deviceID> has format RISEnnn or the device's MAC address in lowercase, with the colon's stripped out and cannot be changed

<deviceName|slugified> will be the device name (as configured in the app) with spaces replaced by underscores

HTTP Endpoints

GET /: list devices. Response type: [String : Device] - ID as String key, Device as value

{
    "RISE148": {
        "id": "RISE148",
        "battery": 29,
        "batteryLevelLastChanged": "2018-05-03T17:14:10.590Z",
        "position": 0,
        "positionLastChanged": "2018-05-03T17:14:10.320Z",
        "connectionState": "connected",
        "state": "closed",
        "group": "Living Room",
        "name": "Living Room1"
    },
    "RISE236": {
        "id": "RISE236",
        "battery": 28,
        "batteryLevelLastChanged": "2018-05-03T17:14:10.497Z",
        "position": 0,
        "positionLastChanged": "2018-05-03T17:14:10.198Z",
        "connectionState": "connected",
        "state": "closed",
        "group": "Living Room",
        "name": "Living Room2"
    }
}

GET /<deviceID>: Get individual device data (or 404 if no device by that ID).

Response type: Device example:

{
    "id": "RISE148",
    "battery": 29,
    "batteryLevelLastChanged": "2018-05-03T17:14:10.590Z",
    "position": 0,
    "positionLastChanged": "2018-05-03T17:14:10.320Z",
    "connectionState": "connected",
    "state": "closed",
    "group": "Living Room",
    "name": "Living Room1"
}

POST /<deviceID>/identify: Ask deviceID to identify itself (beep). Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/move?position=<position>: Ask deviceID to move to <position>. Response type: Device or 404

POST /<deviceID>/stop: Ask deviceID to stop moving. Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/calibrateModeStart: Enable calibrate mode on deviceID. Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/moveUp: Request device to move up. Beware that if calibration mode is enabled, the defined top position is not honoured. Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/stop: Request device to stop moving. Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/moveDown: Request device to move down. Beware that if calibration mode is enabled, the defined bottom position is not honoured. Response type: 200 - OK or 404 - Not Found

POST /<deviceID>/calibrateTop: When in calibrate mode, set the current position as the top-most position. Response Type: 200 - OK or 404 - Not Found

POST /<deviceID>/calibrateBottom: When in calibrate mode, set the current position as the bottom-most position. Response Type: 200 - OK or 404 - Not Found

POST /<deviceID>/calibrateModeStop: Disable calibrate mode on deviceID. Response type; 200 - OK or 404 - Not Found

POST /exit: Quit somactrl (useful if you run with systemd and want to restart). Response type: 200 - OK

Parameters

<deviceID> has format RISEnnn or the device's MAC address in lowercase, with the colon's stripped out and cannot be changed

<position> should be an integer between 0 (closed) and 100 (open)

soma-ctrl's People

Contributors

andersonshatch avatar dependabot-preview[bot] avatar dependabot-support avatar joshlopez-movinganalytics avatar philhawthorne avatar thomasprior avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

soma-ctrl's Issues

Hassio

Is it possible to make this work on my raspberry Pi running Hassio?

And would it not be cool to make this an addon for HA?

TypeError: Cannot read property 'valueHandle' of undefined

Describe the bug
After some amount of time has passed, it appears it lost the connection to the device and is unable to see or control it.

To Reproduce
Steps to reproduce the behavior:

  • Set up the Soma device
  • Wait a couple hours
  • Try to use it
  • No data is reported, and controlling the device does nothing. There is an error in the console.

Screenshots / Logs

Aug 21 06:21:15 hassbian soma.sh[15398]: /home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/gatt.js:535
Aug 21 06:21:15 hassbian soma.sh[15398]:     this._queueCommand(this.writeRequest(characteristic.valueHandle, data, false), function(data) {
Aug 21 06:21:15 hassbian soma.sh[15398]:                                                         ^
Aug 21 06:21:15 hassbian soma.sh[15398]: TypeError: Cannot read property 'valueHandle' of undefined
Aug 21 06:21:15 hassbian soma.sh[15398]:     at Gatt.write (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/gatt.js:535:57)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at NobleBindings.write (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/bindings.js:364:10)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at Noble.write (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/noble/lib/noble.js:325:19)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at Characteristic.write (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/noble/lib/characteristic.js:74:15)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at SomaShade.stop (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/src/SomaShade.js:104:34)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at MqttClient.MQTTConnector.mqttClient.on (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/src/MQTTConnector.js:24:28)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at MqttClient.emit (events.js:180:13)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at MqttClient._handlePublish (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/mqtt/lib/client.js:987:12)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at MqttClient._handlePacket (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/mqtt/lib/client.js:336:12)
Aug 21 06:21:15 hassbian soma.sh[15398]:     at work (/home/homeassistant/.nvm/versions/node/v9.11.2/lib/node_modules/soma-ctrl/node_modules/mqtt/lib/client.js:292:12)
Aug 21 06:21:15 hassbian systemd[1]: soma.service: Main process exited, code=exited, status=1/FAILURE

Versions

  • Node/npm/OS version: v9.11.2
  • Soma-ctrl version: 1.4.2
  • Home Assistant version (if applicable): 0.96.5

Somactrl throwing MTU error, stops responding

Describe the bug
Periodically, seemingly without reason, soma ctrl crashes.

To Reproduce
Steps to reproduce the behavior:

Wish I could reproduce this - happens at least once a day.

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots / Logs

root@debian:~# somactrl -d --mqtt-url REDACTED -u homeassistant -p REDACTED
  soma* No device names supplied, will stop scanning after 30 seconds +0ms
  soma* scanning for as many devices until timeout +3ms
  soma* discovered ef7fc660b28c +132ms
  soma:ef7fc660b28c mqtt topic homeassistant/cover/ef7fc660b28c +0ms
  soma:ef7fc660b28c mqtt connected +23ms
  soma* discovered eaa7e572b88f +119ms
  soma:eaa7e572b88f mqtt topic homeassistant/cover/eaa7e572b88f +0ms
  soma* discovered c6e31a0493d8 +11ms
  soma:c6e31a0493d8 mqtt topic homeassistant/cover/c6e31a0493d8 +0ms
  soma:eaa7e572b88f mqtt connected +14ms
  soma:c6e31a0493d8 mqtt connected +5ms
noble: unknown peripheral c2b66fccd9fa connected!
noble: unknown peripheral c2b66fccd9fa disconnected!
noble: unknown peripheral c2b66fccd9fa connected!
/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/noble.js:564
  peripheral.mtu = mtu;
                 ^

TypeError: Cannot set property 'mtu' of undefined
    at Noble.onMtu (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/noble.js:564:18)
    at NobleBindings.emit (events.js:310:20)
    at NobleBindings.onMtu (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/hci-socket/bindings.js:277:8)
    at Gatt.emit (events.js:310:20)
    at Object.callback (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/hci-socket/gatt.js:329:10)
    at Gatt.onAclStreamData (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/hci-socket/gatt.js:133:26)
    at AclStream.emit (events.js:322:22)
    at AclStream.push (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/hci-socket/acl-stream.js:33:10)
    at NobleBindings.onAclDataPkt (/usr/lib/node_modules/soma-ctrl/node_modules/@abandonware/noble/lib/hci-socket/bindings.js:288:15)
    at Hci.emit (events.js:310:20)

Versions

  • Node/npm/OS version: Node 12.16.2, NPM 6.14.4, Debian Buster stable
  • Soma-ctrl version: 2.0.0
  • Home Assistant version (if applicable): 108.6

Additional context
Same issue occurs on both a noble supported BT adapter and one not on the list, throwing the same MTU error.
Tried forking and updating noble and bt-hci-socket but that has had zero impact on behaviour.

Hard to catch as -d does not appear to log to file and I have to watch the console to catch it. So far it's beyond my ability to trace this issue.

Issues installing - directory not created

Have attempted installation using NodeJS 9.11.2 on Rasbian on a Pi 3B and Debian Buster, both installation commads (sudo npm install -g soma-ctl) result in a seemingly endless loop of errors and retries:

gyp WARN EACCES attempting to reinstall using temporary dev dir "/usr/lib/node_modules/soma-ctrl/node_modules/usb/.node-gyp"
gyp WARN EACCES user "root" does not have permission to access the dev dir "/usr/lib/node_modules/soma-ctrl/node_modules/usb/.node-gyp/9.11.2"

The installation does not complete.

Manually creating the directory at /usr/lib/node_modules/soma-ctrl/node_modules/usb/.node-gyp/ allows the installation to complete.

Command Disallowed 0xc causes a device to become unresponsive

Got two devices set up on Rapsbian 18.04.

After a certain period of time, the following error occurs:

May 25 21:11:12 elysium somactrl[25680]: noble: unknown peripheral null connected!
May 25 21:11:55 elysium somactrl[25680]: 2019-05-25T21:11:55.361Z soma:ca065a773594 disconnected
May 25 21:11:55 elysium somactrl[25680]: 2019-05-25T21:11:55.362Z soma:ca065a773594 connected for 184.284 seconds
May 25 21:11:55 elysium somactrl[25680]: 2019-05-25T21:11:55.471Z soma:ca065a773594 Error: Command Disallowed (0xc)
May 25 21:11:55 elysium somactrl[25680]:     at NobleBindings.onLeConnComplete (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/bindings.js:220:13)
May 25 21:11:55 elysium somactrl[25680]:     at emitOne (events.js:116:13)
May 25 21:11:55 elysium somactrl[25680]:     at Hci.emit (events.js:211:7)
May 25 21:11:55 elysium somactrl[25680]:     at Hci.processCmdStatusEvent (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/hci.js:674:12)
May 25 21:11:55 elysium somactrl[25680]:     at Hci.onSocketData (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/hci.js:469:12)
May 25 21:11:55 elysium somactrl[25680]:     at emitOne (events.js:116:13)
May 25 21:11:55 elysium somactrl[25680]:     at BluetoothHciSocket.emit (events.js:211:7)
May 25 21:11:55 elysium somactrl[25680]: 2019-05-25T21:11:55.472Z soma:ca065a773594 connected!
May 25 21:11:55 elysium somactrl[25680]: noble warning: unknown peripheral ca065a773594
May 25 21:12:08 elysium somactrl[25680]: 2019-05-25T21:12:08.205Z soma:eebe28d042aa disconnected

and earlier this happened to the other device:

ay 25 19:49:19 elysium somactrl[24059]: 2019-05-25T19:49:19.420Z soma:eebe28d042aa disconnected
May 25 19:49:19 elysium somactrl[24059]: 2019-05-25T19:49:19.420Z soma:eebe28d042aa connected for 182.086 seconds
May 25 19:49:19 elysium somactrl[24059]: 2019-05-25T19:49:19.533Z soma:eebe28d042aa Error: Command Disallowed (0xc)
May 25 19:49:19 elysium somactrl[24059]:     at NobleBindings.onLeConnComplete (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/bindings.js:220:13)
May 25 19:49:19 elysium somactrl[24059]:     at emitOne (events.js:116:13)
May 25 19:49:19 elysium somactrl[24059]:     at Hci.emit (events.js:211:7)
May 25 19:49:19 elysium somactrl[24059]:     at Hci.processCmdStatusEvent (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/hci.js:674:12)
May 25 19:49:19 elysium somactrl[24059]:     at Hci.onSocketData (/usr/local/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/hci.js:469:12)
May 25 19:49:19 elysium somactrl[24059]:     at emitOne (events.js:116:13)
May 25 19:49:19 elysium somactrl[24059]:     at BluetoothHciSocket.emit (events.js:211:7)
May 25 19:49:19 elysium somactrl[24059]: 2019-05-25T19:49:19.534Z soma:eebe28d042aa connected!
May 25 19:49:19 elysium somactrl[24059]: noble warning: unknown peripheral eebe28d042aa
May 25 19:49:22 elysium somactrl[24059]: 2019-05-25T19:49:22.996Z soma:ca065a773594 requesting move to 100
May 25 19:49:23 elysium somactrl[24059]: 2019-05-25T19:49:23.015Z soma:eebe28d042aa requesting move to 100

After that, the particular affected device becomes completely unresponsive and stops reacting to MQTT or HTTP endpoint requests and the somactrl needs to be restarted.

I am not a nodejs wizz, but this seems like a case of a simple exception handling?

Recurrent error

Hi - I wonder if you can help. I’m trying to run on a Pi Model 3 B+.
Raspbian Buster.
Node version 9.11.1 (also tried with 9.11.2 (and 10.16.3)
NPM version 5.6.0.

I keep getting the following error. Any ideas?

Thanks very much - this is exactly what I’m looking for, or will be once I get it working! Thanks.

internal/modules/cjs/loader.js:550
throw err;
^

Error: Cannot find module '../build/Release/binding.node'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:548:15)
at Function.Module._load (internal/modules/cjs/loader.js:475:25)
at Module.require (internal/modules/cjs/loader.js:598:17)
at require (internal/modules/cjs/helpers.js:11:18)
at Object. (/usr/local/lib/node_modules/soma-ctrl/node_modules/bluetooth-hci-socket/lib/native.js:3:15)
at Module._compile (internal/modules/cjs/loader.js:654:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
at Module.load (internal/modules/cjs/loader.js:566:32)
at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
at Function.Module._load (internal/modules/cjs/loader.js:498:3)

TypeError: Cannot read property 'subscribe' of undefined

Hi @andersonshatch

We were discussing this a little while ago on the Home Assistant Forums. I'm still having the positionCharacteristic:

pi@raspberrypi:~ $ sudo /opt/nodejs/bin/somactrl -e 1 --mqtt-url mqtt://10.0.1.3
  soma* No device names supplied, will stop scanning after 1 device(s) connect +0ms
  soma* scanning for 1 device(s) [] +39ms
  soma* connected to RISE62 +2s
  soma* all expected devices connected, stopping scan +3ms
/opt/nodejs/lib/node_modules/soma-ctrl/src/SomaShade.js:147
                this.positionCharacteristic.subscribe();
                                            ^

TypeError: Cannot read property 'subscribe' of undefined
    at peripheral.discoverSomeServicesAndCharacteristics (/opt/nodejs/lib/node_modules/soma-ctrl/src/SomaShade.js:147:45)
    at Peripheral.<anonymous> (/opt/nodejs/lib/node_modules/soma-ctrl/node_modules/noble/lib/peripheral.js:101:13)
    at Service.<anonymous> (/opt/nodejs/lib/node_modules/soma-ctrl/node_modules/noble/lib/service.js:53:7)
    at Object.onceWrapper (events.js:324:30)
    at emitOne (events.js:125:13)
    at Service.emit (events.js:221:7)
    at Noble.onCharacteristicsDiscover (/opt/nodejs/lib/node_modules/soma-ctrl/node_modules/noble/lib/noble.js:302:13)
    at emitThree (events.js:145:13)
    at NobleBindings.emit (events.js:227:7)
    at NobleBindings.onCharacteristicsDiscovered (/opt/nodejs/lib/node_modules/soma-ctrl/node_modules/noble/lib/hci-socket/bindings.js:339:8)

Let me know if you need anymore details

“state” does not update

Describe the bug

The “state” string always shows “open”.

To Reproduce
Go to http://YOUR-IP/SOMA-ID

Expected behavior
For this field to show
Closing
Opening
Stopped

(or some equivalent thereof) as appropriate.

Additional context
I’m not sure if this is a bug with the code or a limitation with the data exposed by the Soma units. The reason I am interested is because I am trying to map these outputs into the PositionState characteristic of the WindowCovering service in HomeKit.

Thanks
Mark

trying to make this work for HomeBridge...

Hi!
I do hope you can help me with trying to get this working as a homebridge plugin, although my knowledge about JS is a bit limited. I wrote a test module like
bildschirmfoto 2019-02-27 um 22 44 12
but got a result like
bildschirmfoto 2019-02-27 um 22 40 20
any idea what I am missing here?

all the best
Thomas

Latest version only connect to one device

Hi Josh,

I wanted to connect second device (I have 3 in total) after instelling it all on a PI as the docker setup didn't work in combination with HASS and BLE devices but it seems that the last version only connects to the first device it finds.... it connects to both but not at the same time
I have done various test to make sure that it isn't my installation:

  • timer on 120 seconds - but only finds one...
  • Devices to look for 2 ... same result
  • node /home/pi/node_modules/soma-ctrl/index.js e2e48169e805 fdbdb47f4f7a --url mqtt:192.168.2.192:1883 ----- it always connects to e2e48169e805 ie the first one
  • node /home/pi/node_modules/soma-ctrl/index.js fdbdb47f4f7a e2e48169e805 --url mqtt:192.168.2.192:1883 ----- it always connects to fdbdb47f4f7a ie again the first one

Can you have a look at this please?
Thanks
Ron

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.