diff --git a/README.md b/README.md
index 1b6627b..5cc5d6a 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,23 @@
HomeAssistant integration to ICA shopping list
Install as custom component, manually or using HACS.
-You need to have a valid ICA account and a password (6 digits)
+You need to have a valid ICA account and a password (4-6 digits)
+Optionaliy you can add a default store sorting, 0 = none.
Add in configuration.yaml:
```
ica_shopping_list:
- username: ICA-USERNAME
- listname: My shopping list
- password: ICA PASSWORD
+ username: !secret ica_username
+ listname: My shopping list
+ password: !secret ica_pw
+ storesorting: 0
```
+In your secrets.yaml add:
+```
+ica_username: [USERNAME]
+ica_pw: [4-6 DIGIT PASSWORD]
+```
+
```listname``` is case sensitive.
If the list is not found, it will be created. Space and å, ä, ö is valid.
diff --git a/custom_components/ica_shopping_list/__init__.py b/custom_components/ica_shopping_list/__init__.py
index 8f2e793..a448053 100644
--- a/custom_components/ica_shopping_list/__init__.py
+++ b/custom_components/ica_shopping_list/__init__.py
@@ -8,7 +8,6 @@
import voluptuous as vol
-#from homeassistant.const import HTTP_NOT_FOUND, HTTP_BAD_REQUEST
from homeassistant.core import callback
from homeassistant.components import http
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -18,6 +17,8 @@
from homeassistant.components import websocket_api
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME)
+import aiohttp #handle http requests
+
ATTR_NAME = "name"
DOMAIN = "ica_shopping_list"
@@ -32,6 +33,7 @@
icaUser = None
icaPassword = None
icaList = None
+icaStoreSort = None #default store sorting
EVENT = "shopping_list_updated"
INTENT_ADD_ITEM = "HassShoppingListAddItem"
@@ -70,9 +72,8 @@
{vol.Required("type"): WS_TYPE_SHOPPING_LIST_CLEAR_ITEMS}
)
-
-@asyncio.coroutine
-def async_setup(hass, config):
+#changed from @asyncio.coroutine to async, added await
+async def async_setup(hass, config):
"""Initialize the shopping list."""
global icaUser
icaUser = config["ica_shopping_list"]["username"]
@@ -80,18 +81,21 @@ def async_setup(hass, config):
icaPassword = config["ica_shopping_list"]["password"]
global icaList
icaList = config["ica_shopping_list"]["listname"]
- _LOGGER.debug(config)
+ #added storesorting
+ global icaStoreSort
+ icaStoreSort = config["ica_shopping_list"]["storesorting"]
+
+ #debug cofig/secrets
+ #_LOGGER.debug(config)
- @asyncio.coroutine
- def add_item_service(call):
+ async def add_item_service(call):
"""Add an item with `name`."""
data = hass.data[DOMAIN]
name = call.data.get(ATTR_NAME)
if name is not None:
- data.async_add(name)
+ item_result = await data.async_add(name)
- @asyncio.coroutine
- def complete_item_service(call):
+ async def complete_item_service(call):
"""Mark the item provided via `name` as completed."""
data = hass.data[DOMAIN]
name = call.data.get(ATTR_NAME)
@@ -102,10 +106,10 @@ def complete_item_service(call):
except IndexError:
_LOGGER.error("Removing of item failed: %s cannot be found", name)
else:
- data.async_update(item["id"], {"name": name, "complete": True})
+ await data.async_update(item["id"], {"name": name, "complete": True})
data = hass.data[DOMAIN] = ShoppingData(hass)
- yield from data.async_load()
+ await data.async_load()
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent())
@@ -158,58 +162,83 @@ def __init__(self, hass):
self.items = []
@callback
- def async_add(self, name):
+ async def async_add(self, name):
"""Add a shopping list item."""
self.items = []
- item = json.dumps({"CreatedRows":[{"IsStrikedOver": "false", "ProductName": name}]})
+ # Await the async_add coroutine and store its result in item
+ #item_async = await self.async_add(info.get("name"))
+ # Now you can serialize item to JSON
+
+ articleGroups = {"Välling":9,"Kaffe":9,"Maskindiskmedel":11,"Hushållspapper":11,"Toapapper":11,"Blöjor":11}
+
+ articleGroup = articleGroups.get(name, 12)
+
+ item = json.dumps({"CreatedRows":[{"IsStrikedOver": "false", "ProductName": name, "SourceId": -1, "ArticleGroupId":articleGroup}]})
_LOGGER.debug("Item: " + str(item))
URI = "/api/user/offlineshoppinglists"
- api_data = Connect.post_request(URI, item)
- _LOGGER.debug("Adding product: " + str(item))
- for row in api_data["Rows"]:
- name = row["ProductName"].capitalize()
- uuid = row["OfflineId"]
- complete = row["IsStrikedOver"]
-
- item = {"name": name, "id": uuid, "complete": complete}
- _LOGGER.debug("Item: " + str(item))
- self.items.append(item)
-
+ api_data = await Connect.post_request(URI, item,"/sync")
+
+ if api_data is not None and "Rows" in api_data:
+ _LOGGER.debug("Adding product: " + str(item))
+ for row in api_data["Rows"]:
+ name = row["ProductName"].capitalize()
+ uuid = row["OfflineId"]
+ complete = row["IsStrikedOver"]
+ source = row["SourceId"]
+
+ item = {"name": name, "id": uuid, "complete": complete, "SourceId": source}
+ _LOGGER.debug("Item: " + str(item))
+ self.items.append(item)
+ else:
+ _LOGGER.error("Failed to get data from API, 180, async_add")
+
_LOGGER.debug("Items: " + str(self.items))
return self.items
@callback
- def async_update(self, item_id, info):
+ async def async_update(self, item_id, info):
"""Update a shopping list item."""
- _LOGGER.debug("Info: " + str(info))
+ _LOGGER.debug("Info 200: " + str(item_id) +" - "+ str(info))
self.items = []
-
+
if info.get("complete") == True or info.get("complete") == False:
- item = json.dumps({ "ChangedRows": [ { "OfflineId": item_id, "IsStrikedOver": info.get("complete") } ] })
+
+ # Await the async_add coroutine and store its result in item
+ #completed = await self.async_add(info.get("completed"))
+ _LOGGER.debug('complete???' + str(info.get("complete")))
+ completed = info.get("complete")
+ # Now you can serialize item to JSON
+ item = json.dumps({"ChangedRows": [{"OfflineId": item_id, "IsStrikedOver": completed, "SourceId": -1}]})
elif info.get("name"):
- item = json.dumps({ "ChangedRows": [ { "OfflineId": item_id, "ProductName": info.get("name") } ] })
- _LOGGER.debug("Item: " + str(item))
+ # Await the async_add coroutine and store its result in item
+ item_name = await self.async_add(info.get("name"))
+ # Now you can serialize item to JSON
+ item = json.dumps({"ChangedRows": [{"OfflineId": item_id, "ProductName": item_name, "SourceId": -1}]})
+ _LOGGER.debug("Item 214: " + str(item))
URI = "/api/user/offlineshoppinglists"
- api_data = Connect.post_request(URI, item)
- _LOGGER.debug("Updating product: " + str(item))
- for row in api_data["Rows"]:
- name = row["ProductName"].capitalize()
- uuid = row["OfflineId"]
- complete = row["IsStrikedOver"]
-
- item = {"name": name, "id": uuid, "complete": complete}
- _LOGGER.debug("Item: " + str(item))
- self.items.append(item)
-
+ api_data = await Connect.post_request(URI, item,"/sync")
+
+ if api_data is not None and "Rows" in api_data:
+ _LOGGER.debug("Updating product: " + str(item))
+ for row in api_data["Rows"]:
+ name = row["ProductName"].capitalize()
+ uuid = row["OfflineId"]
+ complete = row["IsStrikedOver"]
+ source = row["SourceId"]
+
+ item = {"name": name, "id": uuid, "complete": complete, "SourceId": source}
+ _LOGGER.debug("Item: " + str(item))
+ self.items.append(item)
+
_LOGGER.debug("Items: " + str(self.items))
return self.items
@callback
- def async_clear_completed(self):
+ async def async_clear_completed(self):
"""Clear completed items."""
completed_items = []
@@ -223,35 +252,42 @@ def async_clear_completed(self):
_LOGGER.debug("Item: " + str(item))
URI = "/api/user/offlineshoppinglists"
- api_data = Connect.post_request(URI, item)
+ api_data = await Connect.post_request(URI, item,"/sync")
_LOGGER.debug("Adding product: " + str(api_data))
for row in api_data["Rows"]:
name = row["ProductName"].capitalize()
uuid = row["OfflineId"]
complete = row["IsStrikedOver"]
-
- item = {"name": name, "id": uuid, "complete": complete}
+ source = row["SourceId"]
+
+ item = {"name": name, "id": uuid, "complete": complete, "SourceId": source}
_LOGGER.debug("Item: " + str(item))
self.items.append(item)
_LOGGER.debug("Items: " + str(self.items))
return self.items
- @asyncio.coroutine
- def async_load(self):
+ async def async_load(self):
"""Load items."""
- def load():
+ async def load():
"""Load the items synchronously."""
URI = "/api/user/offlineshoppinglists"
- api_data = Connect.get_request(URI)
+ api_data = await Connect.get_request(URI)
+ _LOGGER.debug(api_data)
+
+ if api_data is None:
+ _LOGGER.error("Failed to load shopping list data")
+ return
+
_LOGGER.debug("Adding to ica: " + str(api_data))
for row in api_data["Rows"]:
name = row["ProductName"].capitalize()
uuid = row["OfflineId"]
complete = row["IsStrikedOver"]
+ source = row["SourceId"]
- item = {"name": name, "id": uuid, "complete": complete}
+ item = {"name": name, "id": uuid, "complete": complete, "SourceId": source}
_LOGGER.debug("Item: " + str(item))
self.items.append(item)
@@ -259,7 +295,7 @@ def load():
return self.items
# return load_json(self.hass.config.path(PERSISTENCE), default=[])
- self.items = yield from self.hass.async_add_job(load)
+ self.items = await self.hass.async_add_job(load)
def save(self):
"""Save the items."""
@@ -272,16 +308,21 @@ class AddItemIntent(intent.IntentHandler):
intent_type = INTENT_ADD_ITEM
slot_schema = {"item": cv.string}
- @asyncio.coroutine
- def async_handle(self, intent_obj):
+ async def async_handle(self, intent_obj):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"]
- intent_obj.hass.data[DOMAIN].async_add(item)
+
+ # Await the async_add method to get the result
+ result = await intent_obj.hass.data[DOMAIN].async_add(item)
response = intent_obj.create_response()
- response.async_set_speech(f"I've added {item} to your shopping list")
+ item_result = await intent_obj.hass.data[DOMAIN].async_add(item)
+ response.async_set_speech(f"I've added {item_result} to your shopping list")
+
intent_obj.hass.bus.async_fire(EVENT)
+
+ # Return the result
return response
@@ -291,8 +332,7 @@ class ListTopItemsIntent(intent.IntentHandler):
intent_type = INTENT_LAST_ITEMS
slot_schema = {"item": cv.string}
- @asyncio.coroutine
- def async_handle(self, intent_obj):
+ async def async_handle(self, intent_obj):
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response()
@@ -348,10 +388,9 @@ class CreateShoppingListItemView(http.HomeAssistantView):
name = "api:shopping_list:item"
@RequestDataValidator(vol.Schema({vol.Required("name"): str}))
- @asyncio.coroutine
- def post(self, request, data):
+ async def post(self, request, data):
"""Create a new shopping list item."""
- item = request.app["hass"].data[DOMAIN].async_add(data["name"])
+ item = await request.app["hass"].data[DOMAIN].async_add(data["name"])
request.app["hass"].bus.async_fire(EVENT)
return self.json(item)
@@ -429,139 +468,134 @@ def glob_password():
def glob_list():
global icaList
return icaList
+
+ def glob_icaStoreSort():
+ global icaStoreSort
+ return icaStoreSort
@staticmethod
- def get_request(uri):
- """Do API request."""
+ async def get_request(uri):
+ """Do asynchronous API request."""
if Connect.AUTHTICKET is None:
- renewTicket = Connect.authenticate()
+ renewTicket = await Connect.authenticate() # Await authentication
+
Connect.AUTHTICKET = renewTicket["authTicket"]
Connect.listId = renewTicket["listId"]
-
+
url = "https://handla.api.ica.se" + uri + "/" + Connect.listId
headers = {"Content-Type": "application/json", "AuthenticationTicket": Connect.AUTHTICKET}
- req = requests.get(url, headers=headers)
-
- if req.status_code == 401:
- _LOGGER.debug("API key expired. Aquire new")
-
- renewTicket = Connect.authenticate()
- Connect.AUTHTICKET = renewTicket["authTicket"]
- Connect.listId = renewTicket["listId"]
-
- headers = {"Content-Type": "application/json", "AuthenticationTicket": Connect.AUTHTICKET}
- req = requests.get(url, headers=headers)
-
- if req.status_code != 200:
- _LOGGER.exception("API request returned error %d", req.status_code)
-
- else:
- _LOGGER.debug("API request returned OK %d", req.text)
-
- json_data = json.loads(req.content)
- return json_data
-
- elif req.status_code != 200:
- _LOGGER.exception("API request returned error %d", req.status_code)
- else:
- _LOGGER.debug("API request returned OK %d", req.text)
-
- json_data = json.loads(req.content)
- return json_data
+ _LOGGER.debug("URL %s", url)
+
+ async with aiohttp.ClientSession(headers=headers) as session:
+ async with session.get(url) as response:
+ if response.status == 401:
+ # Handle authentication error
+ _LOGGER.debug("API key expired. Acquire new")
+
+ renewTicket = await Connect.authenticate() # Await authentication
+ Connect.AUTHTICKET = renewTicket["authTicket"]
+ Connect.listId = renewTicket["listId"]
+
+ # ... rest of your code
+ elif response.status != 200:
+ _LOGGER.exception("API request returned error,476 %d", response.status)
+ else:
+ _LOGGER.debug("API request returned OK %d", response.status)
+
+ json_data = await response.json() # Await response content
+ _LOGGER.debug(json_data)
+ return json_data
@staticmethod
- def post_request(uri, data):
- """Do API request."""
+ async def post_request(uri, data, ext):
+ """Do asynchronous API request."""
if Connect.AUTHTICKET is None:
- renewTicket = Connect.authenticate()
+ renewTicket = await Connect.authenticate() # Await authentication
Connect.AUTHTICKET = renewTicket["authTicket"]
Connect.listId = renewTicket["listId"]
- url = "https://handla.api.ica.se" + uri + "/" + Connect.listId + "/sync"
- _LOGGER.debug("URL: " + url)
+ url = "https://handla.api.ica.se" + uri + "/" + Connect.listId + ext #ext contains "/sync"
headers = {"Content-Type": "application/json", "AuthenticationTicket": Connect.AUTHTICKET}
- req = requests.post(url, headers=headers, data=data)
-
- if req.status_code == 401:
- _LOGGER.debug("API key expired. Aquire new")
-
- renewTicket = Connect.authenticate()
- Connect.AUTHTICKET = renewTicket["authTicket"]
- Connect.listId = renewTicket["listId"]
-
- headers = {"Content-Type": "application/json", "AuthenticationTicket": Connect.AUTHTICKET}
- req = requests.post(url, headers=headers)
-
- if req.status_code != 200:
- _LOGGER.exception("API request returned error %d", req.status_code)
-
- else:
- _LOGGER.debug("API request returned OK %d", req.text)
-
- json_data = json.loads(req.content)
- return json_data
-
- elif req.status_code != 200:
- _LOGGER.exception("API request returned error %d", req.status_code)
- else:
- _LOGGER.debug("API request returned OK %d", req.text)
-
- json_data = json.loads(req.content)
- return json_data
+ _LOGGER.debug("URL %s", url)
+
+ async with aiohttp.ClientSession(headers=headers) as session:
+ async with session.post(url, data=data) as response:
+ if response.status == 401:
+ # Handle authentication error
+ _LOGGER.debug("API key expired. Acquire new")
+
+ renewTicket = await Connect.authenticate() # Await authentication
+ Connect.AUTHTICKET = renewTicket["authTicket"]
+ Connect.listId = renewTicket["listId"]
+
+ # ... rest of your code
+ elif response.status != 200:
+ _LOGGER.exception("API request returned error, 506 %d", response.status)
+ else:
+ _LOGGER.debug("API request returned OK %d", response.status)
+
+ json_data = await response.json() # Await response content
+ _LOGGER.debug(json_data)
+ return json_data
@staticmethod
- def authenticate():
- """Do API request"""
+ async def authenticate():
+ """Do asynchronous API request"""
icaUser = Connect.glob_user()
icaPassword = Connect.glob_password()
icaList = Connect.glob_list()
listId = None
+ icaStoreSort = Connect.glob_icaStoreSort()
url = "https://handla.api.ica.se/api/login"
- req = requests.get(url, auth=(str(icaUser), str(icaPassword)))
-
- if req.status_code != 200:
- _LOGGER.exception("API request returned error %d", req.status_code)
- else:
- _LOGGER.debug("API request returned OK %d", req.text)
- authTick = req.headers["AuthenticationTicket"]
-
- if Connect.listId is None:
- url = 'https://handla.api.ica.se/api/user/offlineshoppinglists'
- headers = {"Content-Type": "application/json", "AuthenticationTicket": authTick}
- req = requests.get(url, headers=headers)
- response = json.loads(req.content)
-
- for lists in response["ShoppingLists"]:
- if lists["Title"] == icaList:
- listId = lists["OfflineId"]
-
- if Connect.listId is None and listId is None:
- _LOGGER.info("Shopping-list not found: %s", icaList)
- newOfflineId = secrets.token_hex(4) + "-" + secrets.token_hex(2) + "-" + secrets.token_hex(2) + "-"
- newOfflineId = newOfflineId + secrets.token_hex(2) + "-" + secrets.token_hex(6)
- _LOGGER.debug("New hex-string: %s", newOfflineId)
- data = json.dumps({"OfflineId": newOfflineId, "Title": icaList, "SortingStore": 0})
-
- url = 'https://handla.api.ica.se/api/user/offlineshoppinglists'
- headers = {"Content-Type": "application/json", "AuthenticationTicket": authTick}
-
- _LOGGER.debug("List does not exist. Creating %s", icaList)
- req = requests.post(url, headers=headers, data=data)
-
- if req.status_code == 200:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, auth=aiohttp.BasicAuth(icaUser, icaPassword)) as response:
+ if response.status != 200:
+ _LOGGER.exception("API request returned error, 526 %d", response.status)
+ else:
+ _LOGGER.debug("API request returned OK %d", response.status)
+ authTick = response.headers["AuthenticationTicket"]
+
+ if Connect.listId is None:
url = 'https://handla.api.ica.se/api/user/offlineshoppinglists'
headers = {"Content-Type": "application/json", "AuthenticationTicket": authTick}
- req = requests.get(url, headers=headers)
- response = json.loads(req.content)
-
- _LOGGER.debug(response)
-
- for lists in response["ShoppingLists"]:
- if lists["Title"] == icaList:
- listId = lists["OfflineId"]
- _LOGGER.debug(icaList + " created with offlineId %s", listId)
-
- authResult = {"authTicket": authTick, "listId": listId}
- return authResult
+
+ async with session.get(url, headers=headers) as response:
+ response = await response.json()
+ for lists in response["ShoppingLists"]:
+ if lists["Title"] == icaList:
+ listId = lists["OfflineId"]
+
+ if Connect.listId is None and listId is None:
+ _LOGGER.info("Shopping-list not found: %s", icaList)
+ newOfflineId = secrets.token_hex(4) + "-" + secrets.token_hex(2) + "-" + secrets.token_hex(2) + "-"
+ newOfflineId = newOfflineId + secrets.token_hex(2) + "-" + secrets.token_hex(6)
+ _LOGGER.debug("New hex-string: %s", newOfflineId)
+
+ icaStoreSort = 0 if icaStoreSort is None else icaStoreSort
+
+ data = json.dumps({"OfflineId": newOfflineId, "Title": icaList if listId is None else listId, "SortingStore": icaStoreSort})
+
+ url = 'https://handla.api.ica.se/api/user/offlineshoppinglists'
+ headers = {"Content-Type": "application/json", "AuthenticationTicket": authTick}
+
+ _LOGGER.debug("List does not exist. Creating %s", icaList)
+
+ async with session.post(url, headers=headers, data=data) as response:
+ if response.status == 200:
+ url = 'https://handla.api.ica.se/api/user/offlineshoppinglists'
+ headers = {"Content-Type": "application/json", "AuthenticationTicket": authTick}
+
+ async with session.get(url, headers=headers) as response:
+ response = await response.json()
+
+ for lists in response["ShoppingLists"]:
+ if lists["Title"] == icaList:
+ listId = lists["OfflineId"]
+ _LOGGER.debug(icaList + " created with offlineId %s", listId)
+
+ authResult = {"authTicket": authTick, "listId": listId}
+ _LOGGER.debug("authTicket: %s", authTick)
+ _LOGGER.debug("New listId: %s", listId)
+ return authResult