Giter Club home page Giter Club logo

pyelectroluxconnect's Introduction

pyelectroluxconnect

Important

Electrolux has moved user accounts fom Electrolux Connectivity Platform (ECP) to OneApp (OCP) API used by Electrolux (Google Play, App Store) and AEG (Google Play, App Store) Apps. OneApp API is not supported by this client. ECP access is disabled by Electrolux. This client dosn't work anymore.

Python client package to communicate with the Electrolux Connectivity Platform (ECP) used by some home appliances, Electrolux owned brands, like: Electrolux, AEG, Frigidaire, Husqvarna. Tested with AEG washer-dryer, but probably could be used with some internet connected ovens, diswashers, fridges, airconditioners.
It is general client, and all parameters (called HACL), that can be read or set, names and translations are dynamically generated, based on appliance profile file, downloaded from ECP servers.

Compatibility

This package is compatibile with home appliances registered with one of this ECP based mobile apps:

Unsupported devices

This package is not compatibile with appliances controlled with this mobile apps:

Features

  • list appliances paired with Electrolux account
  • get appliance profile with translations
  • get appliance state
  • send command to appliance
  • register/unregister Client with Electrolux MQTT cloud based broker

Usage

Initiate session

To use this library, there's an account with Electrolux/AEG/Frigidaire app must be created. By default, library is using EMEA region apps credentials. If account is created in other region app, region parameter must be set. If created account is not supported, You can manually set regionServer, customApiKey and customApiBrand parameters (from sniffed traffic or extracted from mobile app).

import pyelectroluxconnect
ses = pyelectroluxconnect.Session(username, password, region="emea", tokenFileName = ".electrolux-token", country = "US", language = None, deviceId = "CustomDeviceId", verifySsl = True, regionServer=None, customApiKey=None, customApiBrand=None)

or minimal input set:

import pyelectroluxconnect
ses = pyelectroluxconnect.Session(username, password)

where:
username, password - ECP (Electrolux site) credentials
tokenFileName - file to store auth token (default: ~/.electrolux-token)
region - account region (defalt emea. Tested with emea, apac, na, latam, frigidaire)
country - 2-char country code (default US)
language - 3-char language code for translations (All - for all delivered languages, default: None)
deviceId - custom id of client used in ECP, should be unique for every client instance (default: CustomDeviceId)
verifySsl - verify ECP servers certs (default: True)
regionServer - region server URL (default is based on selected region)
customApiKey - custom value of "x-ibm-client-id" and "x-api-key" HTTP headers (default is based on selected region)
customApiBrand - custom "brand" value (default is based on selected region)

Login to ECP

ses.login()

Get list of appliances registered to Electrolux account

appllist = ses.getAppliances()
print(appllist)

Get appliances connection state

for appliance in appllist:  
	print(ses.getApplianceConnectionState(appliance))

Get appliance profile

List of parameters (HACL's) with allowed values, translations, etc... Note, that not all parameters can be read, or set over ECP.
Each parameter is in "module:hacl" form. Module is internal appliance module symbol, hacl is parameter hex symbol, that can be read from or set to module.

print(ses.getApplianceProfile(appliance))

Get appliance latest state from ECP

Get latest appliance state from ECP. When appliance is online, current state updates are available over Internet with MQTT protocol. To get credentials to connect any MQTT client to ECP MQTT broker, use registerMQTT() method.

to get latest state from a platform:

print(ses.getApplianceState(appliance, paramName = None, rawOutput = False))

paramName - comma separated list of patrameter names (None (default) for all params)
rawOutput - get list of parameters in received form. False (default): parse output to more friendly form (with translations, etc)

Send param value to appliance

Send value to appliance (list of supported appliance destinations (destination) and parameters (hacl) with allowed values (value), You can get with getApplianceProfile() method):

ses.setHacl(appliance, hacl, value, destination)

hacl - hex number of param (HACL)
value - value to set (it could be number or list of parameters (for container HACL type))
destination - destination module name, from profile path (NIU, WD1, etc...)

washer-dryer examples:

  • set Wash+Dry "Cottons" program, with "Extra Dry" dryness Level:
ses.setHacl(appliance, "0x1C09", [{"50":"0x0000"},{"12":"128"},{"6.32":1},{"6.33":1}], "WD1")
  • pause program:
ses.setHacl(appliance, "0x0403", 4, "WD1")

Register client to MQTT broker

print(ses.registerMQTT())

returns parameters required to login to Electrolux MQTT broker with any MQTT client:
Url - Host of MQTT broker (with port number)
OrgId - Organization ID
ClientID - MQTT Client ID
DeviceToken - Token required to authentication (for IBM broker, use string use-token-auth as username, DeviceToken as password)

List of MQTT topics (QoS = 0) to subscribe:

  • iot-2/cmd/live_stream/fmt/+
  • iot-2/cmd/feature_stream/fmt/+

Unregister client from MQTT broker

ses.unregisterMQTT()

Parse received MQTT message

print(ses.getMqttState(mqttJsonPayload))

Parse message from MQTT broker, and return in getApplianceState(...) like form. mqttJsonPayload - MQTT message payload in JSON form.

Very simple MQTT example to receive online appliance state changes from ECP MQTT broker

Please Note: Electrolux moved MQTT broker from IBM servers. After that, this code is not valid.

import paho.mqtt.client as mqtt
import pyelectroluxconnect

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe("iot-2/cmd/live_stream/fmt/json", 0)

def on_message(client, userdata, msg):
    print(ses.getMqttState(msg.payload))

ses = pyelectroluxconnect.Session(login, passwd, language = "pol", region="emea",  deviceId='MQTTHA2')
ses.login()

mqtt_params = ses.registerMQTT()

client = mqtt.Client(client_id = mqtt_params["ClientID"])
client.tls_set(ca_certs = ses.getSSLCert())
client.username_pw_set("use-token-auth", mqtt_params["DeviceToken"])
    
client.on_connect = on_connect
client.on_message = on_message
    
client.connect(mqtt_params["Url"].split(":")[0], int(mqtt_params["Url"].split(":")[1]), 60)
    
while True:
    client.loop()

Disclaimer

This library was not made by AB Electrolux. It is not official, not developed, and not supported by AB Electrolux.

pyelectroluxconnect's People

Contributors

fabaff avatar tomeko12 avatar

Stargazers

 avatar  avatar  avatar  avatar  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  avatar  avatar  avatar

pyelectroluxconnect's Issues

Tag the source

It would be very helpful if you could tag releases. This would enable distributions to fetch the package from GitHub instead of PyPI .

Thanks

Invalid API response: There is no access for user who is different from the session

I’m using the HomeAssistant integration (https://github.com/mauro-midolo/homeassistant_electrolux_status) and basically I have the following issue…
I setup my washing machine (AEG LR86CUC94) and I setup the integration and all was working fine.
Then, my wife made an account and setup the washing machine, and then the appliance was removed from my account….

Now when I tried to add the washing machine in HomeAssistant, I get the following error. I’ve already completely removed the integration and re-added, but I always receive the same. I’ve even deleted my AEG account…. No succes….

2023-07-25 21:01:53.566 ERROR (SyncWorker_5) [pyelectroluxconnect.Session] API response error ECP0210: There is no access for user who is different from the session
2023-07-25 21:01:53.569 ERROR (SyncWorker_5) [pyelectroluxconnect.Session] Exception in _getAppliancesList: Invalid API response: There is no access for user who is different from the session (ECP0210)
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 188, in _getAppliancesList
    _json = self._requestApi(urls.getAppliances(self._username))
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 969, in _requestApi
    raise ResponseError(err.status_code, err.message) from None
pyelectroluxconnect.Session.ResponseError: Invalid API response: There is no access for user who is different from the session (ECP0210)
2023-07-25 21:01:53.577 ERROR (SyncWorker_5) [pyelectroluxconnect.Session] Error while get Appliances list: Invalid API response: There is no access for user who is different from the session (ECP0210)
2023-07-25 21:01:53.582 ERROR (SyncWorker_5) [pyelectroluxconnect.Session] Invalid API response: There is no access for user who is different from the session (ECP0210)
2023-07-25 21:01:53.585 ERROR (MainThread) [custom_components.electrolux_status.config_flow] Invalid API response: There is no access for user who is different from the session (ECP0210)
Traceback (most recent call last):
  File "/config/custom_components/electrolux_status/config_flow.py", line 126, in _test_credentials
    await self.hass.async_add_executor_job(client.login)
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 1073, in login
    raise Exception(ErrorArg) from None
Exception: Invalid API response: There is no access for user who is different from the session (ECP0210)

I made an issue on the integration page as well, but I guess this is where I should actually be.
https://github.com/mauro-midolo/homeassistant_electrolux_status/issues/122

match attr : invalid syntax in Session.py

Some syntax error on Debian Python 3.9.2 (default, Feb 28 2021, 17:03:44)

Traceback (most recent call last):
  File "/opt/scripts/elux.py", line 2, in <module>
    import pyelectroluxconnect
  File "/usr/local/lib/python3.9/dist-packages/pyelectroluxconnect/__init__.py", line 11, in <module>
    from .Session import (
  File "/usr/local/lib/python3.9/dist-packages/pyelectroluxconnect/Session.py", line 313
    match attr:
          ^
SyntaxError: invalid syntax

Works ok when removing the section

            result = {}
            for attr in ["group", "brand", "model_name"]:
                if attr in _json and _json[attr] != "":
                    result["model" if attr == "model_name" else attr] = _json[attr]
#                else:
#                    match attr:
#                        case "group":
#                            result["group"] = ""
#                        case "brand":
#                            result["brand"] = "Electrolux"
#                        case "model_name":
#                            result["model"] = self._findModel(pnc, elc)[0]
            return result

Electrolux unstable API - possibility of retries and backoffs?

Hi

I'm using this library through an integration in Home Assistant - and we're seeing a lot of "dropouts" where the status of entities will disappear only to reappear a few minutes later. This happens regularly throughout the day.

My theory is that there are no retries anywhere in either that plugin, or this library. So any failed request will lead to this behaviour.

Having little to no python experience - would it be possible to add retries to this library in the HTTP request method, such that it will retry N times (f.ex. 2), with some backoff time (f.ex. 10 seconds and 60 seconds) ?

Thanks in advance.
Mike.

Missing info(?)

In manual for connecting to MQTT there is a reference to use-token-auth as username. I cannot find infor about use-token-auth anywhere else? Please advice.

Translation of Electrolux API

Hello @tomeko12 ,
I use Home Assistant is used integration "homeassistant_electrolux_status.

During using this integration were found
A. few not translated text ("Remote Control Enabled for No Safety Relevant Operations" or "Applicance in Normal Mode")
B. one wrong translated text (wrongly "není dostupné" right "nie je dostupné")

After investigation and communication with author of described integration was obtained answer - the text is direct part of Electrolux API ("the text and any translations are read directly from Electrolux's API").

If I am right, you are using same connector, and maybe you may help me with right path to solve.
Thank you.

No longer able to login :(

I think the same thing as #9 #10 is happening again. Have confirmed the credentials work in the app.

>>> import pyelectroluxconnect
>>> ses = pyelectroluxconnect.Session(username, password, region="apac")
>>> ses.login()

Getting new auth token
Unauthorized (401)

Unable to set HACL with string values

Discovered while messing around with mauro-midolo/homeassistant_electrolux_status#38

Some HACL values require to be set as a string, but in Session.py:682-686 all string are converted to int values.
It should be possible to check up against the profile data_format, which in my case is "string" rather than "uint_16".

No longer able to login

Started receiving "Unauthorized (401)" out of the blue. Double checked and the credentials work on AEG Care app.

Using this to test:

import os
import pprint
import pyelectroluxconnect
ses = pyelectroluxconnect.Session("[email protected]", "XXXXXXXX", region="EMEA")
ses.login()
appllist = ses.getAppliances()
d = open(f"devices.json", "w")
d.write(str(appllist))
d.close()
for appliance in appllist:
    # print(ses.getApplianceConnectionState(appliance))
    p = open(f"{appllist[appliance]['alias']}-profil.json", "w")
    p.write(str(ses.getApplianceProfile(appliance)))
    p.close()
    s = open(f"{appllist[appliance]['alias']}-state.json", "w")
    s.write(str(ses.getApplianceState(appliance, rawOutput=False)))
    s.close()
    rs = open(f"{appllist[appliance]['alias']}-rawstate.json", "w")
    rs.write(str(ses.getApplianceState(appliance, rawOutput=True)))
    rs.close()

output:

Exception in _getAppliancesList: Invalid API response: Unauthorized (401)
Traceback (most recent call last):
  File "/home/user/.local/lib/python3.10/site-packages/pyelectroluxconnect/Session.py", line 186, in _getAppliancesList
    _json = self._requestApi(urls.getAppliances(self._username))
  File "/home/user/.local/lib/python3.10/site-packages/pyelectroluxconnect/Session.py", line 978, in _requestApi
    raise ResponseError(errcode, message) from None
pyelectroluxconnect.Session.ResponseError: Invalid API response: Unauthorized (401)
Error while get Appliances list: Invalid API response: Unauthorized (401)
Invalid API response: Unauthorized (401)
Traceback (most recent call last):
  File "/mnt/t/electrolux/test.py", line 5, in <module>
    ses.login()
  File "/home/user/.local/lib/python3.10/site-packages/pyelectroluxconnect/Session.py", line 1071, in login
    raise Exception(ErrorArg) from None
Exception: Invalid API response: Unauthorized (401)

Are Air Conditoners Supported?

Does anyone know if the AEG Comfort 6000 models are able to be controlled this way?

I'm in the UK, and have the AEG app working (so presumably emea?) to controll the aircon.

When I try using this code, the only region that recognises my account is emea, however, ses.getAppliances() returns an empty dictionary.

my quickly hacked example code;

import pyelectroluxconnect

def connect(region):

    username = "my@email"
    password = "my_password"

    ses = pyelectroluxconnect.Session(username=username, password=password, tokenFileName = ".electrolux-token", region=region)
    print(region)
    try:
        ses.login()
    except :
        print("no account")
        return
    appllist = ses.getAppliances()
    print(appllist)

regions = ["emea", "apac", "na", "latam", "frigidaire"]

for region in regions:
    connect(region)

returns;

emea
{}
apac
API token error ECP0105: Session key not found
Getting new auth token
API response error AER0802: No Consumer with this email: my@email
No Consumer with this email:my@email(AER0802)
no account
na
API token error ECP0105: Session key not found
Getting new auth token
User is not valid: my@email(ECP0108)
no account
latam
API token error ECP0105: Session key not found
Getting new auth token
authentication_failed (AER0802)
no account
frigidaire
API token error ECP0105: Session key not found
Getting new auth token
User is not valid: my@email (ECP0108)
no account

pyelectroluxconnect.Session.LoginError: Unauthorized (401) when using "frigidaire" region

I use another repository that has this as a dependency and started getting errors logging in a few hours ago. I noticed the recent commit 5605321 for APAC region, but I have a frigidaire product (so region="frigidaire"). I suspect the "frigidaire" endpoint may also need to be updated upstream (here) - I tried looking for info on this but wasn't able to get anywhere.

Steps to recreate: I updated my local pyelectroluxconnect to 0.3.17 and did the following:

Python 3.11.3 (tags/v3.11.3:f3909b8, Apr  4 2023, 23:49:59) [MSC v.1934 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> username = "[email protected]"
>>> password = "hopeitsagoodone"
>>> import pyelectroluxconnect
>>> ses = pyelectroluxconnect.Session(username, password, region="frigidaire")
>>> ses.login()
Getting new auth token
Unauthorized (401)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python311\Lib\site-packages\pyelectroluxconnect\Session.py", line 1061, in login
    self._createToken()
  File "C:\Python311\Lib\site-packages\pyelectroluxconnect\Session.py", line 176, in _createToken
    raise LoginError(f'{err.message} ({err.status_code})') from None
pyelectroluxconnect.Session.LoginError: Unauthorized (401)

Related issue in other repo is https://github.com/mauro-midolo/homeassistant_electrolux_status/issues/115

Unable to get device configuration file

Since i think the change to 3.16 i've got this issue. It used to work with 3.15 (home assistent integration)

It can connect and auth with the api because i do get my own PNC, ELC and SN returned. 1st line in the errormsg is edited for security reasons!

I tested it with the following code on the latest version:

import pyelectroluxconnect
ses = pyelectroluxconnect.Session("[email protected]", "XXX", region="emea", tokenFileName = ".electrolux-token", country = "NL", language = None, deviceId = "CustomDeviceId", verifySsl = True)
ses.login()

And the errormsg

Exception in _getApplianceConfiguration(PNC, ELC and SN edited for security reasons): Unable to get device configuration file.
Traceback (most recent call last):
  File "/home/orangepi/.local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 277, in _getApplianceConfiguration
    raise Exception("Unable to get device configuration file.")
Exception: Unable to get device configuration file.
Exception in _getAppliancesList: Unable to get device configuration file.
Traceback (most recent call last):
  File "/home/orangepi/.local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 202, in _getAppliancesList
    applianceProfile = self._getApplianceConfiguration(self._applianceIndex[device["appliance_id"]]["pnc"],
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/orangepi/.local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 280, in _getApplianceConfiguration
    raise Error(err) from None
pyelectroluxconnect.Session.Error: Unable to get device configuration file.
Error while get Appliances list: Unable to get device configuration file.
Unable to get device configuration file.
Traceback (most recent call last):
  File "/home/orangepi/test.py", line 3, in <module>
    ses.login()
  File "/home/orangepi/.local/lib/python3.11/site-packages/pyelectroluxconnect/Session.py", line 1073, in login
    raise Exception(ErrorArg) from None
Exception: Unable to get device configuration file.

MQTT api changed

I tried using session.registerMQTT()
It doesn't work anymore

missing parameter for utils.registerMQTT(region)
Also API returns probably changed structure than it used to:

POST https://api.eu.ecp.electrolux.com/user-appliance-reg/api/v1.1/devices

{
	"status": "OK",
	"code": "ECP0000",
	"message": "Device created",
	"data": {
		"mqttUrl": "mqtt.eu.ecp.electrolux.com:443",
		"topic": "mobile/cmd/d:mobile:-1000000000_000000002",
		"featureTopic": "mobile/feature/d:mobile:-1000000000_000000002",
		"clientId": "d:mobile:-1000000000_000000002"
	}
}

so it no longer contains DeviceToken

Where did you find those APIs? Mobile app?
I decompiled Android apk com.electrolux.oneapp.android.aeg, but couldn't find any references to MQTT

Cache the model names when acquired from Electrolux.

Hi,

I'm using this project, where I reported an issue regarding a log message I'm seeing a lot.

I didn't get any response on it, so maybe I'll have more luck here :).

I'm seeing this log message, all the time, it seems like it occurs on every update of my electrolux devices, which is a lot. Each log line seems to represent a request to the Electrolux site, bombarding them with unecessary requests. I've looked at the code in this project, and I can see there is some caching somehow, but I can't understand it. So I can't workaround this by ensuring whatever cache isn't being populated properly, is prepared.

Nov 15 22:30:35 mbwodr01 homeassistant[559]: 2022-11-15 22:30:35.512 INFO (SyncWorker_4) [pyelectroluxconnect.Session] No model name in profile file, try to find in other sites
Nov 15 22:30:35 mbwodr01 homeassistant[559]: 2022-11-15 22:30:35.512 INFO (SyncWorker_4) [pyelectroluxconnect.Session] Trying to get model 914555442_02 info from https://www.electrolux-ui.com/ website
Nov 15 22:30:37 mbwodr01 homeassistant[559]: 2022-11-15 22:30:37.112 INFO (SyncWorker_4) [pyelectroluxconnect.Session] No model name in profile file, try to find in other sites
Nov 15 22:30:37 mbwodr01 homeassistant[559]: 2022-11-15 22:30:37.113 INFO (SyncWorker_4) [pyelectroluxconnect.Session] Trying to get model 916098764_00 info from https://www.electrolux-ui.com/ website
Nov 15 22:31:08 mbwodr01 homeassistant[559]: 2022-11-15 22:31:08.320 INFO (SyncWorker_5) [pyelectroluxconnect.Session] No model name in profile file, try to find in other sites
Nov 15 22:31:08 mbwodr01 homeassistant[559]: 2022-11-15 22:31:08.320 INFO (SyncWorker_5) [pyelectroluxconnect.Session] Trying to get model 914555442_02 info from https://www.electrolux-ui.com/ website
Nov 15 22:31:09 mbwodr01 homeassistant[559]: 2022-11-15 22:31:09.665 INFO (SyncWorker_5) [pyelectroluxconnect.Session] No model name in profile file, try to find in other sites
Nov 15 22:31:09 mbwodr01 homeassistant[559]: 2022-11-15 22:31:09.666 INFO (SyncWorker_5) [pyelectroluxconnect.Session] Trying to get model 916098764_00 info from https://www.electrolux-ui.com/ website

I'm writing here in the hopes that this can be fixed, such that both the log message disappears, and that Electrolux can relax a bit .. :)

The messages appear every 30 seconds, once for each device - of which I have two, so that's roughly 5.760 requests each day to Electrolux to get .. a model name .. :)

Mike.

Can't login in APAC region.

Login fails with traceback -

Traceback (most recent call last):
File "/config/custom_components/electrolux_status/config_flow.py", line 84, in _test_credentials
await self.hass.async_add_executor_job(client.login)
File "/usr/local/lib/python3.9/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/local/lib/python3.9/site-packages/pyelectroluxconnect/Session.py", line 689, in login
self._getAppliancesList()
File "/usr/local/lib/python3.9/site-packages/pyelectroluxconnect/Session.py", line 185, in _getAppliancesList
"alias": device["nickname"],
KeyError: 'nickname'

Nickname not in dictionary. instead it has appliance_type:

{'appliance_type': '914900813_00', 'pnc': '914900813', 'elc': '00', 'sn': '02024136', 'mac': '443E07061EDA', 'cpv': '00', 'group': 'FabricCare', 'brand': 'Electrolux', 'model': 'EWF1041'}

I modified Session.py and confirmed this is the only issue.

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.