├── LICENSE ├── README.md └── barcode_reader.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Grocy Py Scanner 2 | This awesome script will allow you the user to add/remove items from the system with just a barcode scanner 3 | 4 | The script requires that you edit a few of the variables at the top of the description 5 | 6 | UPC_DATABASE_API='UPC_API' 7 | 8 | buycott_token='BUYCOTT_API' 9 | 10 | GROCY_API='GROCY_API' 11 | 12 | This is some arbitrary number. We used a barcode generator and put in some random 7 digit number to use as our ADDID. You could use a barcode off of something that you will never inventory 13 | 14 | add_id='ADDID' 15 | 16 | This is the URL of your grocy app or IP address. 17 | 18 | base_url = 'YOUR_GROCY_URL' 19 | 20 | This is where you want the product to go. I created an entry ADDED_UPDATE_LOCATION so that it stands out when going through the prduct list so I know it needs to be addressed. 21 | 22 | This could be enhanced with barcodes that represent each location you have, then have the main function do some stuff but eh not worth it for me at this time. 23 | 24 | This can be found by clicking on the location in the app and looking at the URL of course if you're curl savvy you could always query the api directly. 25 | 26 | location_id = LOCATION_ID 27 | 28 | Optional 29 | 30 | homeassistant_url='YOUR_HOME_ASSISTANT_URL/DOMAIN/SERVICE' 31 | 32 | homeassistant_api='YOUR_HOME_ASSISTANT_API' 33 | 34 | I wrote this because I knew there was no way I would be able to get the house on board with using this if they needed to run around with a computer with the scanner attached to it. 35 | 36 | So instead I plugged the dongle for my scanner into a raspberry pi that runs my sprinklers and I just run the app from there. Right now I run it in a screen session so that I can see the output. 37 | 38 | I'm working on coming up with some sort of a solution to let the user know if the scan worked. Right now best idea I have come up with is to make another API call to my HomeAssistant and then have HomeAssistant do something that informs the user that it worked. 39 | 40 | ## How To Use This 41 | Put this script on whatever machine you want. Your local laptop, raspberry pi it doesn't matter just as long as wherever you put it, is the same place as your barcode scanner. In my case its on my raspberry pi that is running OpenSprinkler because 9 months out of the year that pi is just sitting idle. It doesn't matter where you run this script from though as long as it can make the necessary API calls that it needs to do. If you want to run it on the same machine that you have grocy thats cool too. In my case Grocy is on my docker VM so that was not an option for me hence why I put it on the pi. YMMV. If you have any questions please let me know. 42 | 43 | Once you have the script installed just execute it with ./barcode_reader.py I don't have it setup to do logging so it will just print everything to the screen which is handy so you can see what its doing. I'll probably implement logging down the road though. If you don't want to see what its doing just add a & to the end of the command like ./barcode_reader.py & 44 | 45 | ## Update: 46 | 47 | 04/29/2019: I got the home assistant integration working. For me I have this going to an Echo that is in my kitchen and I have it speak what mode it is in and if I added something it tells me how many it added it by. If this is the first time addding it will say zero because the quantity hasn't been setup properly in the database yet. Once you update the system to the correct quantity for the item then it will work just fine. 48 | 49 | The Text To Speech is a little delayed because well it has to make the call to HomeAssistant which in turn has to make the call to convert the Text To Speech to whatever you have playing it. 50 | 51 | I am fully open to pull requests as I am certain there are enhancements that someone who is a little more python savvy than myself could come up with or perhaps optimize the code a little more 52 | 53 | Right now when its adding you need to let it chunk through that bit before scanning. Once everything is added to the system and its just doing increase/decrease operations you can scan pretty fast because its all local calls. 54 | 55 | I have buycott in there because they allow you to have 7 days free before they bill you for the service. I'll probably be changing the order of the services but haven't decided yet. 56 | 57 | 12/11/2019: Did a lot of code optimization in here to try and clean things up. Also had to put in some error handling for Wal-Mart because they're returning what python says is invalid json so instead of the script puking and dying it now handles the error and keeps moving along. 58 | 59 | Enjoy! 60 | -------------------------------------------------------------------------------- /barcode_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import signal, sys 3 | import json 4 | import requests 5 | 6 | from evdev import InputDevice, categorize, ecodes 7 | 8 | #This will allow you to work the grocy inventory with a barcode scanner 9 | #I found I needed to just declare some things right off the bat when the app loads such as the ADD and barcode 10 | UPC_DATABASE_API='' 11 | #I've been waiting for my token but you can register for one at developers.walmart.com 12 | walmart_token='' 13 | #This UPC lookup doesn't require a token or anything 14 | walmart_search=1 15 | #This UPC DB also doesn't require a token but does have a limit of 100 calls per day 16 | upc_item_db=1 17 | #Your Grocy API Key 18 | GROCY_API='' 19 | #This is some arbitrary number. We used a barcode generator and put in some random 7 digit number to use as our ADDID. You could use a barcode off of something that you will never inventory 20 | add_id='43235735' 21 | #This is the URL of your grocy app or IP address. 22 | base_url = 'http://grocy.thefuzz4.net/api' 23 | ADD = 0 24 | barcode = '' 25 | message_text = '' 26 | #This is where you want the product to go. I created an entry ADDED_UPDATE_LOCATION so that it stands out when going through the prduct list so I know it needs to be addressed. 27 | #This could be enhanced with barcodes that represent each location you have, then have the main function do some stuff but eh not worth it for me at this time 28 | location_id = 6 29 | device = InputDevice('/dev/input/event0') # Replace with your device 30 | scancodes = { 31 | 11: u'0', 32 | 2: u'1', 33 | 3: u'2', 34 | 4: u'3', 35 | 5: u'4', 36 | 6: u'5', 37 | 7: u'6', 38 | 8: u'7', 39 | 9: u'8', 40 | 10: u'9' 41 | } 42 | NOT_RECOGNIZED_KEY = u'X' 43 | homeassistant_url='YOUR_HOME_ASSISTANT_URL/DOMAIN/SERVICE' 44 | homeassistant_api='YOUR_HOME_ASSISTANT_API' 45 | 46 | def increase_inventory(upc): 47 | global response_code 48 | global product_name 49 | #Need to lookup our product_id, if we don't find one we'll do a search and add it 50 | product_id_lookup(upc) 51 | print ("Increasing %s") % (product_name) 52 | url = base_url+"/stock/products/%s/add" % (product_id) 53 | data = {'amount': purchase_amount, 54 | 'transaction_type': 'purchase'} 55 | #We have everything we need now in order to complete the rest of this function 56 | grocy_api_call_post(url, data) 57 | #As long as we get a 200 back from the app it means that everything went off without a hitch 58 | if response_code != 200: 59 | print ("Increasing the value of %s failed") % (product_name) 60 | else: 61 | if homeassistant_token != '': 62 | message_text="I increased %s by a count of %s to a total count of %s" % (product_name, purchase_amount, stock_amount) 63 | homeassistant_call(message_text) 64 | barcode = '' 65 | 66 | def decrease_inventory(upc): 67 | global response_code 68 | #Going to see if we can find a product_id, if we don't find it we'll add it. Problem here though is that we need to have a quantity in the system. If there isn't any we'll error because there is nothing to decrease. This is ok 69 | product_id_lookup(upc) 70 | print("Stepping into the decrease") 71 | #Lets make sure we can actually decrease this before we get too crazy 72 | if stock_amount > 0: 73 | print ("Decreasing %s by 1") % (product_name) 74 | url = base_url+"/stock/products/%s/consume" % (product_id) 75 | data = {'amount': 1, 76 | 'transaction_type': 'consume', 77 | 'spoiled': 'false'} 78 | #We now have everything we need and we can now proceed 79 | grocy_api_call_post(url, data) 80 | if response_code == 400: 81 | print ("Decreasing the value of %s failed, are you sure that there was something for us to decrease?") % (product_name) 82 | message_text=("I failed to decrease %") % (product_name) 83 | homeassistant_call(message_text) 84 | else: 85 | print ("The current stock amount for %s is 0 so there was nothing for us to do here") % (product_name) 86 | if homeassistant_token != '': 87 | message_text=("The stock amount for %s was zero, so there was nothing for me to decrease") % (product_name) 88 | homeassistant_call(message_text) 89 | if homeassistant_token != '' and response_code == 200: 90 | message_text=("Consumed %s. You now have %s left") % (product_name, stock_amount) 91 | homeassistant_call(message_text) 92 | barcode='' 93 | 94 | def product_id_lookup(upc): 95 | #Need to declare this as a global and we'll do this again with a few others because we need them elsewhere 96 | global product_id 97 | print("Looking up the product_id") 98 | #Lets check to see if the UPC exists in grocy 99 | url = base_url+"/stock/products/by-barcode/%s" % (upc) 100 | headers = { 101 | 'cache-control': "no-cache", 102 | 'GROCY-API-KEY': GROCY_API 103 | } 104 | r = requests.get(url, headers=headers) 105 | r.status_code 106 | print (r.status_code) 107 | #Going to check and make sure that we found a product to use. If we didn't find it lets search the internets and see if we can find it. 108 | if r.status_code == 400: 109 | message_text=("I did not have the product locally so I am now going to search for it and add it to the system") 110 | if homeassistant_token != '': 111 | homeassistant_call(message_text) 112 | upc_lookup(upc) 113 | else: 114 | j = r.json() 115 | global product_id 116 | product_id = j['product']['id'] 117 | global purchase_amount 118 | purchase_amount = j['product']['qu_factor_purchase_to_stock'] 119 | global product_name 120 | product_name = j['product']['name'] 121 | print ("Our product_id is %s") % (product_id) 122 | global stock_amount 123 | stock_amount = j['stock_amount'] 124 | 125 | def upc_lookup(upc): 126 | found_it=0 127 | if walmart_search == 1 and found_it==0: 128 | print("Looking up in Wal-Mart Search") 129 | url = "https://search.mobile.walmart.com/v1/products-by-code/UPC/%s?storeId=1" % (upc) 130 | headers = { 131 | 'Content-Type': 'application/json', 132 | 'cache-control': "no-cache" 133 | } 134 | try: 135 | r = requests.get(url=url, headers=headers) 136 | j = r.json() 137 | if r.status_code == 200: 138 | print("Walmart Search found it so now we're going to gather some info here and then add it to the system") 139 | if 'data' in j: 140 | name = j['data']['common']['name'] 141 | description = '' 142 | #We now have what we need to add it to grocy so lets do that 143 | #Sometimes buycott returns a success but it never actually does anything so lets just make sure that we have something 144 | add_to_system(upc, name, description) 145 | found_it=1 146 | if r.status_code == 400: 147 | found_it=0 148 | except ValueError: 149 | print("We failed to decode the json for this item %s") % (upc) 150 | message_text=("Walmart errored on this item. Stupid walmart") 151 | homeassistant_call(message_text) 152 | except requests.exceptions.Timeout: 153 | print("The connection timed out") 154 | except requests.exceptions.TooManyRedirects: 155 | print ("Too many redirects") 156 | except requests.exceptions.RequestException as e: 157 | print (e) 158 | if upc_item_db == 1 and found_it==0: 159 | #This is a free service that limits you to 100 hits per day if we can't find it here we'll still create it in the system but it will be just a dummy entry 160 | print("Looking up in UPCItemDB") 161 | url = "https://api.upcitemdb.com/prod/trial/lookup?upc=%s" % (upc) 162 | headers = { 163 | 'Content-Type': 'application/json', 164 | 'cache-control': "no-cache" 165 | } 166 | try: 167 | r = requests.get(url=url, headers=headers) 168 | j = r.json() 169 | if r.status_code == 200: 170 | if 'total' in j: 171 | total = j['total'] 172 | if total != 0: 173 | print("UPCItemDB found it so now we're going to gather some info here and then add it to the system") 174 | name=j['items'][0]['title'] 175 | description = j['items'][0]['description'] 176 | #We now have what we need to add it to grocy so lets do that 177 | add_to_system(upc, name, description) 178 | found_it=1 179 | except requests.exceptions.Timeout: 180 | print("The connection timed out") 181 | except requests.exceptions.TooManyRedirects: 182 | print ("Too many redirects") 183 | except requests.exceptions.RequestException as e: 184 | print (e) 185 | if ean_data_db == 1 and found_it==0: 186 | #This is a free service that limits you to 100 hits per day if we can't find it here we'll still create it in the system but it will be just a dummy entry 187 | print("Looking up in EANDB") 188 | url = "https://eandata.com/feed/?v=3&keycode=%s&mode=json&find=011110018427" % (ean_data_db, upc) 189 | headers = { 190 | 'Content-Type': 'application/json', 191 | 'cache-control': "no-cache" 192 | } 193 | try: 194 | r = requests.get(url=url, headers=headers) 195 | j = r.json() 196 | if r.status_code == 200: 197 | if 'product' in j: 198 | total = j['total'] 199 | if total != 0: 200 | print("UPCItemDB found it so now we're going to gather some info here and then add it to the system") 201 | name = j['product'][0]['attributes']['product'] 202 | description='' 203 | #We now have what we need to add it to grocy so lets do that 204 | add_to_system(upc, name, description) 205 | found_it=1 206 | except requests.exceptions.Timeout: 207 | print("The connection timed out") 208 | except requests.exceptions.TooManyRedirects: 209 | print ("Too many redirects") 210 | except requests.exceptions.RequestException as e: 211 | print (e) 212 | if found_it==0: 213 | print ("The item with %s was not found so we're adding a dummy one") % (upc) 214 | name="The product was not found in the external sources you will need to fix %s" % (upc) 215 | description='dummy' 216 | add_to_system(upc, name, description) 217 | found_it=1 218 | if found_it==1: 219 | #By now we have our product added to the system. We can now lookup our product_id again and then proceed with whatever it is we were doing 220 | product_id_lookup(upc) 221 | else: 222 | message_text=("I was unable to find a productID for %s and it is not in the system") 223 | homeassistant_call(message_text) 224 | print(message_text) 225 | 226 | #Rather than have this in every section of the UPC lookup we just have a function that we call for building the json for the api call to actually add it to the system 227 | def add_to_system(upc, name, description): 228 | url = base_url+"/objects/products" 229 | data ={"name": name, 230 | "description": description, 231 | "barcode": upc, 232 | "location_id": location_id, 233 | "qu_id_purchase": 1, 234 | "qu_id_stock":0, 235 | "qu_factor_purchase_to_stock": 1, 236 | "default_best_before_days": -1 237 | } 238 | grocy_api_call_post(url, data) 239 | if response_code==204: 240 | print("Just added %s to the system") % (name) 241 | product_id_lookup(upc) 242 | else: 243 | print("Adding the product with %s failed") % (upc) 244 | 245 | #This is a function that is referred to a lot through out the app so its easier for us to just use it as a function rather than type it out over and over 246 | def grocy_api_call_post(url, data): 247 | headers = { 248 | 'cache-control': "no-cache", 249 | 'GROCY-API-KEY': GROCY_API 250 | } 251 | try: 252 | r = requests.post(url=url, json=data, headers=headers) 253 | r.status_code 254 | global response_code 255 | response_code = r.status_code 256 | print (r.status_code) 257 | except requests.exceptions.Timeout: 258 | print("The connection timed out") 259 | except requests.exceptions.TooManyRedirects: 260 | print ("Too many redirects") 261 | except requests.exceptions.RequestException as e: 262 | print (e) 263 | 264 | def homeassistant_call(message_text): 265 | print("Calling homeassistant to speak some text") 266 | headers = { 267 | 'Authorization': 'Bearer {}'.format(homeassistant_token), 268 | 'Content-Type': 'application/json' 269 | } 270 | data = { "message":message_text, 271 | "data":{"type":"tts"}, 272 | "target":["media_player.jason_s_2nd_echo_show_2"] 273 | } 274 | r = requests.post(url=homeassistant_url, json=data, headers=headers) 275 | r.status_code 276 | if r.status_code != 200: 277 | print("HomeAssistant call failed with a status code of %s") % (r.status_code) 278 | 279 | 280 | for event in device.read_loop(): 281 | if event.type == ecodes.EV_KEY: 282 | eventdata = categorize(event) 283 | if eventdata.keystate == 1: # Keydown 284 | scancode = eventdata.scancode 285 | if scancode == 28: # Enter 286 | print (barcode) 287 | if barcode != '' and len(barcode) >= 7: 288 | if barcode == add_id and ADD == 0: 289 | ADD = 1 290 | barcode='' 291 | print("Entering add mode") 292 | if homeassistant_token != '': 293 | message_text="Entering add mode" 294 | homeassistant_call(message_text) 295 | elif barcode == add_id and ADD == 1: 296 | ADD = 0 297 | barcode='' 298 | print("Entering consume mode") 299 | if homeassistant_token != '': 300 | message_text="Entering consume mode" 301 | homeassistant_call(message_text) 302 | elif ADD == 1: 303 | upc=barcode 304 | barcode='' 305 | increase_inventory(upc) 306 | elif ADD == 0: 307 | upc=barcode 308 | barcode='' 309 | decrease_inventory(upc) 310 | 311 | else: 312 | key = scancodes.get(scancode, NOT_RECOGNIZED_KEY) 313 | barcode = barcode + key 314 | if key == NOT_RECOGNIZED_KEY: 315 | print('unknown key, scancode=' + str(scancode)) 316 | --------------------------------------------------------------------------------