Skip to content
This repository has been archived by the owner on Feb 21, 2023. It is now read-only.

Commit

Permalink
Enedis api (#29)
Browse files Browse the repository at this point in the history
* Use the official enedis API
* Use enedis API 2
* add customers v3 api
* Create an API class, and change client to a wrapper. Try to keep Client API intact.
  • Loading branch information
lasconic committed May 7, 2020
1 parent deebe18 commit 62bbcc8
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 118 deletions.
17 changes: 14 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ pyLinky
:target: https://requires.io/github/Pirionfr/pyLinky/requirements/?branch=master
:alt: Requirements Status

Get your consumption data from your Enedis account (www.enedis.fr)
Get your consumption data from your Enedis account (www.enedis.fr)

In order to use the library, you need an account on https://datahub-enedis.fr/.
You need to create a Data Connect Application and as of May 2020 you will get
your client_id by mail and your client_secret by SMS after a week or so...
With these credentials, you can only access the Sandbox environment... You need
to sign a contract and probably wait more to get Production credentials...

The library uses requests and requests_oauthlib to cope with the OAuth 2.0
protocol. It uses an AbstractAuth class to let a developer override the refresh
token function and route it via their own external service.
It also let the storage of the token between sessions to the developer.


This library This is based on jeedom_linky, created by Outadoc (https://github.com/Asdepique777/jeedom_linky)

Installation
------------
Expand All @@ -35,7 +46,7 @@ Usage
-----
Print your current data

pylinky -u <USERNAME> -p <PASSWORD>
pylinky -c <client_id> -s <client_secret> -u <redirect_url>

Dev env
-------
Expand Down
2 changes: 2 additions & 0 deletions pylinky/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from pylinky.client import AbstractAuth
from pylinky.client import LinkyAPI
from pylinky.client import LinkyClient
109 changes: 99 additions & 10 deletions pylinky/__main__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,119 @@
import argparse
import sys
import json
from urllib.parse import urlparse, parse_qs

from pylinky import LinkyClient
from pylinky import LinkyAPI, AbstractAuth, LinkyClient

import logging
import contextlib
from http.client import HTTPConnection

def main():
"""Main function"""
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--username',
required=True, help='enedis username')
parser.add_argument('-p', '--password',
required=True, help='Password')
parser.add_argument('-c', '--client-id',
required=True, help='Client ID from Enedis')
parser.add_argument('-s', '--client-secret',
required=True, help='Client Secret from Enedis')
parser.add_argument('-u', '--redirect-url',
required=True, help='Redirect URL as stated in the Enedis admin console')
parser.add_argument('-t', '--test-consumer',
required=False, help='Test consumer for sandbox 0-9')
parser.add_argument('-v', '--verbose',
required=False, action='store_true', help='Verbose, debug network calls')
args = parser.parse_args()

client = LinkyClient(args.username, args.password)
if (args.verbose):
'''Switches on logging of the requests module.'''
HTTPConnection.debuglevel = 2
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

test_consumer = args.test_consumer

auth = AbstractAuth(client_id=args.client_id, client_secret=args.client_secret, redirect_url=args.redirect_url)
linky_api = LinkyAPI(auth)

try:
client.login()
client.fetch_data()
authorization_url = linky_api.get_authorisation_url(test_customer=test_consumer)
print("Please go to \n{}\nand authorize access.".format(authorization_url))
authorization_response = input("Enter the full callback URL :\n")
authorization_response_qa = parse_qs(urlparse(authorization_response).query)

code = authorization_response_qa["code"][0]
state = authorization_response_qa["state"][0]

token = linky_api.request_tokens(code)
# Not needed, just a test to make sure that refresh_tokens works
token = auth.refresh_tokens()

usage_point_ids = linky_api.get_usage_point_ids()


for usage_point_id in usage_point_ids:
print(usage_point_id)

response = linky_api.get_customer_identity(usage_point_id)
print("get_customer_identity")
print(response.content)
#input("Press a key")

response = linky_api.get_customer_contact_data(usage_point_id)
print("get_customer_contact_data")
print(response.content)
#input("Press a key")

response = linky_api.get_customer_usage_points_contracts(usage_point_id)
print("get_customer_usage_points_contracts")
print(response.content)
#input("Press a key")

response = linky_api.get_customer_usage_points_addresses(usage_point_id)
print("get_customer_usage_points_addresses")
print(response.content)
#input("Press a key")

response = linky_api.get_consumption_load_curve(usage_point_id, "2020-03-01", "2020-03-05")
print("get_consumption_load_curve")
print(response.content)
#input("Press a key")

response = linky_api.get_production_load_curve(usage_point_id, "2020-03-01", "2020-03-05")
print("get_production_load_curve")
print(response.content)
#input("Press a key")

response = linky_api.get_daily_consumption_max_power(usage_point_id, "2020-03-01", "2020-03-05")
print("get_daily_consumption_max_power")
print(response.content)
#input("Press a key")

response = linky_api.get_daily_consumption(usage_point_id, "2020-03-01", "2020-03-05")
print("get_daily_consumption")
print(response.content)
#input("Press a key")

response = linky_api.get_daily_production(usage_point_id, "2020-03-01", "2020-03-05")
print("get_daily_production")
print(response.content)
#input("Press a key")

linky_client = LinkyClient(auth)
linky_client.fetch_data()
data = linky_client.get_data()
print(data)


except BaseException as exp:
print(exp)
return 1
finally:
client.close_session()
print(json.dumps(client.get_data(), indent=2))
linky_api.close_session()
linky_client.close_session()


if __name__ == '__main__':
Expand Down
109 changes: 109 additions & 0 deletions pylinky/abstractauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from typing import Optional, Union, Callable, Dict

from requests import Response
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import TokenExpiredError
from urllib.parse import urlencode


AUTHORIZE_URL_SANDBOX = "https://gw.hml.api.enedis.fr/dataconnect/v1/oauth2/authorize"
ENDPOINT_TOKEN_URL_SANDBOX = "https://gw.hml.api.enedis.fr/v1/oauth2/token"
METERING_DATA_BASE_URL_SANDBOX = "https://gw.hml.api.enedis.fr"

AUTHORIZE_URL_PROD = "https://gw.prd.api.enedis.fr/dataconnect/v1/oauth2/authorize"
ENDPOINT_TOKEN_URL_PROD = "https://gw.prd.api.enedis.fr/v1/oauth2/token"
METERING_DATA_BASE_URL_PROD = "https://gw.prd.api.enedis.fr"

class AbstractAuth:
def __init__(
self,
token: Optional[Dict[str, str]] = None,
client_id: str = None,
client_secret: str = None,
redirect_url: str = None,
token_updater: Optional[Callable[[str], None]] = None,
sandbox: bool = True
):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_url = redirect_url
self.token_updater = token_updater
self.sandbox = sandbox

extra = {"client_id": self.client_id, "client_secret": self.client_secret}

self._oauth = OAuth2Session(
auto_refresh_kwargs=extra,
client_id=client_id,
token=token,
token_updater=token_updater,
)

def authorization_url(self, duration: str="", test_customer: str=""):
"""test state will be appended to state for sandbox testing, it can be 0 to 9"""
url = AUTHORIZE_URL_PROD
if (self.sandbox):
url = AUTHORIZE_URL_SANDBOX
state = self._oauth.new_state()
if test_customer:
state = state + test_customer
return self._oauth.authorization_url(url, duration=duration, state=state)

def refresh_tokens(self) -> Dict[str, Union[str, int]]:
"""Refresh and return new tokens."""
url = ENDPOINT_TOKEN_URL_PROD
if (self.sandbox):
url = ENDPOINT_TOKEN_URL_SANDBOX
if self.redirect_url is not None:
url = url + "?" + urlencode({'redirect_uri': self.redirect_url})
token = self._oauth.refresh_token(url, include_client_id=True, client_id=self.client_id, client_secret=self.client_secret, refresh_token=self._oauth.token['refresh_token'])

if self.token_updater is not None:
self.token_updater(token)

return token

def request_tokens(self, code) -> Dict[str, Union[str, int]]:
"""return new tokens."""
url = ENDPOINT_TOKEN_URL_PROD
if (self.sandbox):
url = ENDPOINT_TOKEN_URL_SANDBOX
if self.redirect_url is not None:
url = url + "?" + urlencode({'redirect_uri': self.redirect_url})
token = self._oauth.fetch_token(url, include_client_id=True, client_id=self.client_id, client_secret=self.client_secret, code=code)

if self.token_updater is not None:
self.token_updater(token)
return token

def request(self, path: str, arguments: Dict[str, str]) -> Response:
"""Make a request.
We don't use the built-in token refresh mechanism of OAuth2 session because
we want to allow overriding the token refresh logic.
"""
url = METERING_DATA_BASE_URL_PROD
if (self.sandbox):
url = METERING_DATA_BASE_URL_SANDBOX
url = url + path
# This header is required by v3/customers, v4/metering data is ok with the default */*
headers = {'Accept': "application/json"}
try:
response = self._oauth.request("GET", url, params=arguments, headers=headers)
if (response.status_code == 403):
self._oauth.token = self.refresh_tokens()
else:
return response
except TokenExpiredError:
self._oauth.token = self.refresh_tokens()

return self._oauth.request("GET", url, params=arguments, headers=headers)

def get_usage_point_ids(self):
if not self._oauth.token or not self._oauth.token.get("usage_points_id"):
return []
return self._oauth.token['usage_points_id'].split(",")

def close(self):
if self._oauth:
self._oauth.close()
self._oauth = None

0 comments on commit 62bbcc8

Please sign in to comment.