From e454fe54d7f38dccdde3a0be5d2dc11b10755b7a Mon Sep 17 00:00:00 2001 From: Martin Wiorek Date: Fri, 29 Sep 2023 21:37:36 +0200 Subject: [PATCH 1/2] Update README.md Changed readme with credentials moved to secrets, and added storsorting --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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. From 2ef7da57cc5e01a79e11f2b0e7b8ca16bfd72b47 Mon Sep 17 00:00:00 2001 From: Martin Wiorek Date: Fri, 29 Sep 2023 21:52:21 +0200 Subject: [PATCH 2/2] Update __init__.py Major changes to make work with latest hass core and python version. services work, shipping list ui works partly --- .../ica_shopping_list/__init__.py | 378 ++++++++++-------- 1 file changed, 206 insertions(+), 172 deletions(-) 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