Repository: hectorespert/python-oligo (language: python)

Issue:
Add AsyncIO version
First of all, thanks for this library it works like a charm.

I needed an asyncio version of this library for a personal project. I tried to maintain same methods and structure to keep it easy to maintain and they both can coexists in the same library.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index d5573c3..b200b42 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ pip install oligo
 ```
 ### Ejemplos:
 
-#### Consultar consumo actual:
+#### Consultar consumo actual (Sync):
 
 ```python
 from oligo import Iber
@@ -23,7 +23,23 @@ connection.login("user", "password")
 watt = connection.watthourmeter()
 print(watt)
 ```
-#### Consultar estado ICP interno:
+
+#### Consultar consumo actual (ASync):
+
+```python
+from oligo import AIOIber
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+
+    watt = await connection.watthourmeter()
+    print(watt)
+
+asyncio.run(main())
+```
+
+#### Consultar estado ICP interno (Sync):
 
 ```python
 from oligo import Iber
@@ -33,12 +49,26 @@ status = connection.icpstatus()
 print(status)
 ```
 
-#### Obtener el consumo horario durante un periodo
+#### Consultar estado ICP interno (ASync):
+
+```python
+from oligo import AIOIber
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+    status = await connection.icpstatus()
+    print(status)
+
+asyncio.run(main())
+```
+
+#### Obtener el consumo horario durante un periodo (Sync)
 
 ```python
 from oligo import Iber
 from datetime import date, timedelta
-    
+
 connection = Iber()
 connection.login("user", "password")
 
@@ -50,6 +80,26 @@ consumo = connection.consumption(from_date, until_date)
 print(consumo[:10])
 ```
 
+#### Obtener el consumo horario durante un periodo (ASync)
+
+```python
+from oligo import AIOIber
+from datetime import date, timedelta
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+
+    from_date = date.today() - timedelta(days=7)
+    until_date = date.today() - timedelta(days=1)
+
+    consumo = await connection.consumption(from_date, until_date)
+
+    print(consumo[:10])
+
+asyncio.run(main())
+```
+
 Los datos son el consumo por hora en Watt-horas. En este caso tendremos los
 dato de una semana, que son 7 por 24, 168 valores. Si sumamos y dividimos
 por 1000, tenemos el consumo de una semana en kWh.
@@ -61,7 +111,7 @@ por 1000, tenemos el consumo de una semana en kWh.
 pip install oligo
 ```
 ### Example:
-#### Obtain current consumption:
+#### Obtain current consumption (Sync):
 
 ```python
 from oligo import Iber
@@ -72,7 +122,22 @@ connection.login("user", "password")
 watt = connection.watthourmeter()
 print(watt)
 ```
-#### Get ICP status:
+#### Obtain current consumption (ASync):
+
+```python
+from oligo import AIOIber
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+
+    watt = await connection.watthourmeter()
+    print(watt)
+
+asyncio.run(main())
+```
+
+#### Get ICP status (Sync):
 
 ```python
 from oligo import Iber
@@ -82,7 +147,21 @@ status = connection.icpstatus()
 print(status)
 ```
 
-#### Retrieve the hourly consumption during a time period
+#### Get ICP status (ASync):
+
+```python
+from oligo import AIOIber
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+    status = await connection.icpstatus()
+    print(status)
+
+asyncio.run(main())
+```
+
+#### Retrieve the hourly consumption during a time period (Sync)
 
 ```python
 from oligo import Iber
@@ -99,6 +178,26 @@ consumo = connection.consumption(from_date, until_date)
 print(consumo[:10])
 ```
 
+#### Retrieve the hourly consumption during a time period (Async)
+
+```python
+from oligo import AIOIber
+from datetime import date, timedelta
+
+async def main():
+    connection = AIOIber()
+    await connection.login("user", "password")
+
+    from_date = date.today() - timedelta(days=7)
+    until_date = date.today() - timedelta(days=1)
+
+    consumo = await connection.consumption(from_date, until_date)
+
+    print(consumo[:10])
+
+asyncio.run(main())
+```
+
 The values are the consumption in Watt-hours. In this case, we have the data
 of one week, which are 7 times 24, 168 values. If we sum and divide by 1000,
 we will have the total consumption from one week in kWh.
diff --git a/oligo/asyncio/__init__.py b/oligo/asyncio/__init__.py
new file mode 100644
index 0000000..4b36a24
--- /dev/null
+++ b/oligo/asyncio/__init__.py
@@ -0,0 +1,1 @@
+from .asynciber import AsyncIber
diff --git a/oligo/asyncio/asynciber.py b/oligo/asyncio/asynciber.py
new file mode 100644
index 0000000..6b5b57b
--- /dev/null
+++ b/oligo/asyncio/asynciber.py
@@ -0,0 +1,183 @@
+try:
+    import aiohttp
+except ImportError:
+    raise RuntimeError("AsyncIber requires aiohttp")
+
+from datetime import datetime
+from typing import Union, Optional
+
+LOGIN_URL = "loginNew/login"
+WATTHOURMETER_URL = "escenarioNew/obtenerMedicionOnline/24"
+ICP_STATUS_URL = "rearmeICP/consultarEstado"
+CONTRACTS_URL = "cto/listaCtos/"
+CONTRACT_DETAIL_URL = "detalleCto/detalle/"
+CONTRACT_SELECTION_URL = "cto/seleccion/"
+OBTENER_ESCENARIOS_URL = "escenarioNew/obtenerEscenariosRest/"
+OBTENER_ESCENARIO_URL = "escenarioNew/refrescarEscenario/"
+GUARDAR_ESCENARIO_URL = "escenarioNew/confirmarMedicionOnLine/{}/1/{}"
+BORRAR_ESCENARIO_URL = "escenarioNew/borrarEscenario/"
+OBTENER_PERIODO_URL = "consumoNew/obtenerDatosConsumoPeriodo/fechaInicio/{}00:00:00/fechaFinal/{}00:00:00/"
+
+
+class ResponseException(Exception):
+    pass
+
+
+class LoginException(Exception):
+    pass
+
+
+class SessionException(Exception):
+    pass
+
+
+class NoResponseException(Exception):
+    pass
+
+
+class SelectContractException(Exception):
+    pass
+
+
+class AsyncIber:
+    def __init__(self) -> None:
+        """Iber class __init__ method."""
+        self.__session = None
+
+    async def close(self):
+        await self.__session.close()
+
+    async def __request(
+        self, path: str, data: Optional[Union[list, dict]] = None
+    ) -> dict:
+        if not self.__session:
+            raise SessionException(
+                "Session required, use login() method to obtain a session"
+            )
+        if data is None:
+            response = await self.__session.get(
+                f"https://www.i-de.es/consumidores/rest/{path}",
+                headers={"accept": "application/json; charset=utf-8"},
+            )
+        else:
+            response = await self.__session.post(
+                f"https://www.i-de.es/consumidores/rest/{path}",
+                json=data,
+                headers={"accept": "application/json; charset=utf-8"},
+            )
+        if response.status != 200:
+            self.__session = None
+            raise ResponseException("Response error, code: {}".format(response.status))
+        data = await response.json()
+        if not data:
+            raise NoResponseException
+        return data
+
+    async def login(self, user: str, password: str) -> bool:
+        """Creates session with your credentials"""
+        self.__session = aiohttp.ClientSession()
+        payload = [
+            user,
+            password,
+            None,
+            "Linux -",
+            "PC",
+            "Chrome 77.0.3865.90",
+            "0",
+            "",
+            "s",
+        ]
+        data = await self.__request(LOGIN_URL, data=payload)
+        if data["success"] != "true":
+            self.__session = None
+            raise LoginException("Login error, bad login")
+        return True
+
+    async def measurement(self) -> dict:
+        """Returns a measurement from the powermeter."""
+        data = await self.__request(WATTHOURMETER_URL)
+        return {
+            "id": data["codSolicitudTGT"],
+            "consumption": data["valMagnitud"],
+            "icp": data["valInterruptor"],
+            "raw_response": data,
+        }
+
+    async def watthourmeter(self) -> float:
+        """Returns your current power consumption."""
+        return await self.measurement()["consumption"]
+
+    async def icpstatus(self) -> bool:
+        """Returns the status of your ICP."""
+        data = await self.__request(ICP_STATUS_URL, data="")
+        return data["icp"] == "trueConectado"
+
+    async def contracts(self) -> Optional[dict]:
+        data = await self.__request(CONTRACTS_URL)
+        if data["success"]:
+            return data["contratos"]
+        return None
+
+    async def contract(self) -> dict:
+        return await self.__request(CONTRACT_DETAIL_URL)
+
+    async def contractselect(self, id: str) -> bool:
+        data = await self.__request(CONTRACT_SELECTION_URL + id)
+        if not data["success"]:
+            raise SelectContractException
+        return True
+
+    async def scene_list(self) -> dict:
+        data = await self.__request(OBTENER_ESCENARIOS_URL)
+        return {"scene_names": data["y"]["smps"], "raw_response": data}
+
+    async def scene_get(self, name: str) -> dict:
+        data = await self.__request(OBTENER_ESCENARIO_URL, data={"nomEscenario": name})
+        return {
+            "name": data["nomEscenario"],
+            "description": data["descripcion"],
+            "consumption": data["numLcaInsta"],
+            "raw_response": data,
+        }
+
+    async def scene_save(
+        self, consumption: str, measurement_id: str, description: str
+    ) -> dict:
+        data = await self.__request(
+            GUARDAR_ESCENARIO_URL.format(consumption, measurement_id),
+            data={
+                "nomEscenario": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
+                "descripcion": description,
+            },
+        )
+        return {"name": data["nomEscenario"], "raw_response": data}
+
+    async def scene_delete(self, name: str) -> bool:
+        await self.__request(BORRAR_ESCENARIO_URL, data={"nomEscenario": name})
+        return True
+
+    async def _consumption_raw(self, start: datetime, end: datetime) -> list:
+        return await self.__request(
+            OBTENER_PERIODO_URL.format(
+                start.strftime("%d-%m-%Y"), end.strftime("%d-%m-%Y")
+            )
+        )
+
+    # Get consumption data from a time period
+    #
+    # start/end: datetime.date objects indicating the time period (both inclusive)
+    # The supported time range seems to be (not documented) from Jan 1 in the previous year and a max
+    # length of one year.
+    #
+    # Returns a list of consumptions starting a midnight on the start day until 23:00 on the last day.
+    # Each value is the hourly consumption in Wh.
+    async def consumption(self, start: datetime, end: datetime) -> list:
+        data = await self._consumption_raw(start, end)
+        return [float(x["valor"]) for x in data["y"]["data"][0] if x]
+
+    # Get total consumption in Wh (Watt-hour) over a time period
+    #
+    # start/end: datetime.date objects indicating the time period (both inclusive)
+    async def total_consumption(self, start, end) -> float:
+        data = await self._consumption_raw(start, end)
+        return float(data["acumulado"])
diff --git a/setup.py b/setup.py
index 2966864..4a995df 100644
--- a/setup.py
+++ b/setup.py
@@ -1,21 +1,24 @@
-import setuptools
+from setuptools import setup, find_packages
 
 with open("README.md", "r") as fh:
     long_description = fh.read()
 
-setuptools.setup(
+setup(
     name="oligo",
-    version="1.2.0",
+    version="1.3.0",
     author="hectorespert",
     author_email="hectorespertpardo@gmail.com",
     description="UNOFFICIAL Python client for i-DE",
     long_description=long_description,
     long_description_content_type="text/markdown",
     url="https://github.com/hectorespert/python-oligo",
-    packages=setuptools.find_packages(),
+    packages=find_packages(),
     install_requires=[
-        'requests',
+        'requests'
     ],
+    extras_require={
+        'asyncio':  ['aiohttp']
+    },
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: MIT License",


Test patch:
diff --git a/tests/test_aioiber.py b/tests/test_aioiber.py
new file mode 100644
index 0000000..f4114ed
--- /dev/null
+++ b/tests/test_aioiber.py
@@ -0,0 +1,14 @@
+import unittest
+
+from oligo.asyncio import AsyncIber
+
+
+class TestAsyncIber(unittest.TestCase):
+
+    def test_instance(self):
+        instance = AsyncIber()
+        self.assertIsInstance(instance, AsyncIber)
+
+
+if __name__ == '__main__':
+    unittest.main()