├── Dockerfile ├── README.md ├── app.py ├── config_example.ini ├── docker-compose.yml ├── document ├── GM805条码识别模块用户手册V1.2.pdf ├── hardware-connection.png ├── scanner-setting-1.png ├── scanner-setting-2.png ├── scanner-setting-3.png └── shell.png ├── esp8266 ├── BarcodeScanner.ino ├── back.stl ├── base.stl └── body.stl ├── gpc_brick_code.json ├── requirements.txt ├── spider ├── __init__.py └── barcode_spider.py └── templates └── index.html /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.18 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 7 | 8 | COPY . . 9 | 10 | EXPOSE 9288 11 | 12 | CMD [ "python", "./app.py" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 特性 2 | 3 | - 条码识别,支持国产商品与进口产品; 4 | - Grocy物品扫码出库; 5 | - Grocy已有物品扫码入库,新物品自动获取商品详情并入库;(商品详情包括:条码,基础信息,图片,GPC类别,保质期判别等) 6 | 7 | # 快速开始 8 | 9 | Grocy配置,Web界面中: 10 | - `设置`-`管理API密钥`-`添加` 11 | - `管理主数据`-`位置`- 根据自身情况添加 12 | - `管理主数据`- `自定义字段`- `添加`- 表单信息:实体:products;名称GDSInfo;标题:GDSInfo;类型:单行文本,勾选"在表格中显示此列" 13 | - 配置`数量单位`:`数量单位`-`添加` 14 | 15 | ```shell 16 | docker pull osnsyc/grocycompanioncn:latest 17 | ``` 18 | 19 | 创建 config.ini 和 docker-compose.yml 文件 20 | 21 | ```ini 22 | # config.ini 23 | [Grocy] 24 | GROCY_URL = http://EXAMPLE.COM 25 | GROCY_PORT = 443 26 | GROCY_API = YOUR_GROCY_API_KEY 27 | # GROCY_DEFAULT_QUANTITY_UNIT_ID 在 shell内获取: 28 | ; curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/quantity_units' \ -H 'accept: application/json' \ -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ | echo -e "$(cat)" 29 | GROCY_DEFAULT_QUANTITY_UNIT_ID = 1 # 默认的数量单位ID 30 | GROCY_DEFAULT_BEST_BEFORE_DAYS = 365 # 默认的保质期天数 31 | # 存储位置ID,与scanner.ino内的位置名称对应 32 | # shell内获取,替换以下地址\端口\api_key: 33 | ; curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/locations' \ 34 | ; -H 'accept: application/json' \ 35 | ; -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ 36 | ; | echo -e "$(cat)" 37 | [GrocyLocation] 38 | pantry = 1 39 | temporary_storage = 2 40 | fridge = 3 41 | living_room = 4 42 | bedroom = 5 43 | bathroom = 6 44 | # 注册RapidAPI账号,并在https://rapidapi.com/Glavier/api/barcodes1/的Pricing点击订阅(免费),复制Endpoints中的X_RapidAPI_Key于此处 45 | [RapidAPI] 46 | X_RapidAPI_Key = YOUR_RapidAPI_API_KEY 47 | ``` 48 | 其中,`GROCY_DEFAULT_QUANTITY_UNIT_ID`的获取方法: 49 | ```shell 50 | curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/quantity_units' \ 51 | -H 'accept: application/json' \ 52 | -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ 53 | | echo -e "$(cat)" 54 | ``` 55 | 56 | 其中,`GrocyLocation`id的获取方法: 57 | ```shell 58 | curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/locations' \ 59 | -H 'accept: application/json' \ 60 | -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ 61 | | echo -e "$(cat)" 62 | ``` 63 | 64 | ```yml 65 | # docker-compose.yml 66 | version: "3" 67 | services: 68 | spider: 69 | image: osnsyc/grocycompanioncn:latest 70 | restart: always 71 | ports: 72 | - "9288:9288" 73 | volumes: 74 | - ./config.ini:/usr/src/app/config.ini 75 | # - ./u2net.onnx:/root/.u2net/u2net.onnx 76 | networks: 77 | - grocy_cn_campanion 78 | 79 | networks: 80 | grocy_cn_campanion: 81 | ``` 82 | 83 | `u2net.onnx`为rembg的模型,程序第一次运行时会自动下载,下载缓慢的也可[手动下载](https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx),放入`docker-compose.yml`同目录,并反注释以下一行 84 | ```yml 85 | - ./u2net.onnx:/root/.u2net/u2net.onnx 86 | ``` 87 | ```shell 88 | docker compose up -d 89 | ``` 90 | 91 | 打开`http://127.0.0.1:9288`,看到页面显示`GrocyCNCompanion Started!`,服务已成功运行. 92 | 93 | GrocyCompanionCN api测试 94 | 95 | ```shell 96 | curl -X POST -H "Content-Type: application/json" -d '{"client":"temporary_storage","aimid":"]E0","barcode":"8935024140147"}' http://127.0.0.1:9288/add 97 | ``` 98 | 99 | 刷新Grocy,出现新物品 100 | 101 | # 硬件 102 | 103 | 条码/二维码扫描模块为GM805,带蜂鸣器和灯光,3.3V供电 104 | 105 | https://img.alicdn.com/imgextra/i3/343151/O1CN01QbKGVt1Z9CkLwAzKp_!!343151.jpg 106 | 107 | 使用模块包括: 108 | - GM805 109 | - ESP01 110 | - AMS1117 111 | - 限位开关 112 | - 3.7V锂电池 113 | 114 | 以下为硬件接线示意图 115 | 116 | ![Alt text](./document/hardware-connection.png) 117 | 118 | 119 | ## 程序烧录 120 | 121 | 完成以下设置并烧入ESP01 122 | ```c 123 | // ./esp8266/BarcodeScanner.ino 124 | #include 125 | #include 126 | 127 | #define SERVER "http://YOUR_SEVER_IP:9288" //GrocyCompanionCN api的地址 128 | #define CLIENT "temporary_storage" //对应config.ini中GrocyLocation的值 129 | #define GPIO0_PIN 0 130 | #define LED_PIN 2 131 | #define HTTP_CODE_OK 200 132 | #ifndef STASSID 133 | #define STASSID "YOUR_SSID" //WiFi名 134 | #define STAPSK "YOUR_PASSWORD" //WiFi密码 135 | #endif 136 | ``` 137 | ## 扫码模块的设置 138 | 139 | 使用模块直接扫描以下二维码设置,设置成功,发出蜂鸣。 140 | 141 | ### 添加AIM ID(必选) 142 | 143 | ![scanner-setting-1](./document/scanner-setting-1.png) 144 | 145 | ### 添加串口输出(必选) 146 | 147 | ![scanner-setting-2](./document/scanner-setting-2.png) 148 | 149 | ### 照明(自选) 150 | 151 | ![scanner-setting-3](./document/scanner-setting-3.png) 152 | 153 | ## 扫码模块测试 154 | 155 | - 切换拨动开关至“出库”模式;(必须为“出库”模式才可正常启动) 156 | - 模块上电自动启动,并发出蜂鸣声,蓝灯亮起; 157 | - WiFi连接成功,蓝灯熄灭; 158 | - 扫码成功,模块发出蜂鸣声; 159 | - 请求成功,模块蓝灯常亮0.5秒后熄灭; 160 | - 请求错误,模块蓝灯连续闪烁2秒; 161 | - 按下微动开关,模块断电; 162 | - 切换拨动开关,切换“出库”和“入库”模式; 163 | 164 | ## 壳体 165 | 166 | ![shell](./document/shell.png) 167 | 168 | ## 物料清单与参考链接 169 | 170 | 171 | | 序号 | 名称 | 参考链接 | 172 | | ---- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | 173 | | 1 | GM805 | [颜色分类:默认](https://item.taobao.com/item.htm?_u=pnh8ujp653f&id=670772947536&spm=a1z09.2.0.0.63572e8d9a6VqT) | 174 | | 2 | ESP01 | [颜色分类:ESP-01S](https://item.taobao.com/item.htm?_u=pnh8ujp6e5a&id=664680861283&spm=a1z09.8149145.0.0.3de7269aKjcPDW) | 175 | | 3 | AMS1117 | [颜色分类:AMS1117-3.3模块 3脚](https://detail.tmall.com/item.htm?_u=pnh8ujp8a63&id=650922269502&spm=a1z09.2.0.0.63572e8dhXa8kZ) | 176 | | 4 | 拨动开关 | [颜色分类:柄高4mm](https://detail.tmall.com/item.htm?_u=pnh8ujpab3e&id=679299103183&spm=a1z09.2.0.0.63572e8dhXa8kZ) | 177 | | 5 | 限位开关 | [颜色分类:KW12 3脚21mm圆弧柄](https://detail.tmall.com/item.htm?_u=pnh8ujp3181&id=706873714470&spm=a1z09.2.0.0.63572e8dhXa8kZ) | 178 | | 6 | 3.7V锂电池 | [颜色分类:3.7V并联加厚600mAh/XH2.54反向插](https://item.taobao.com/item.htm?_u=pnh8ujp152e&id=643433296669&spm=a1z09.2.0.0.63572e8d9a6VqT&skuId=4799818249667) | 179 | | 7 | 锂电池充电器 | [颜色分类:500mA/XH2.54反向母插转换头](https://item.taobao.com/item.htm?_u=pnh8ujp28f4&id=16985757260&skuId=3484166170023&spm=a1z09.2.0.0.63572e8d9a6VqT) | 180 | | 8 | PCB跳线 | | 181 | | 9 | PCB洞洞板 | [颜色分类:双面喷锡 2x8cm](https://detail.tmall.com/item.htm?_u=pnh8ujp1c7f&id=667259213547&spm=a1z09.2.0.0.63572e8dhXa8kZ) | 182 | | 10 | ESP烧录器 | [颜色分类:ESP-01/01S CH340芯片](https://item.taobao.com/item.htm?_u=pnh8ujp6e5a&id=664680861283&skuId=4961471522476&spm=a1z09.2.0.0.67002e8dPzgKnw) | 183 | | 11 | 注塑铜螺母 | [颜色分类:M2x4x3.5](https://detail.tmall.com/item.htm?_u=tnh8ujp4e44&id=673367228604&spm=a1z09.2.0.0.3ab22e8dJ2V8vu) | 184 | | 12 | 螺丝 | M2x6 | 185 | # 鸣谢 186 | 187 | - https://github.com/tenlee2012/BarCodeQuery 188 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # coding = utf-8 3 | import requests 4 | import json 5 | import configparser 6 | 7 | from rembg import remove 8 | from flask import Flask, request, jsonify, render_template 9 | from pygrocy import Grocy, EntityType 10 | 11 | from spider.barcode_spider import BarCodeSpider 12 | 13 | config = configparser.ConfigParser() 14 | config.read('config.ini') 15 | GROCY_URL = config.get('Grocy', 'GROCY_URL') 16 | GROCY_PORT = config.getint('Grocy', 'GROCY_PORT') 17 | GROCY_API = config.get('Grocy', 'GROCY_API') 18 | GROCY_DEFAULT_QUANTITY_UNIT_ID = config.getint('Grocy', 'GROCY_DEFAULT_QUANTITY_UNIT_ID') 19 | GROCY_DEFAULT_BEST_BEFORE_DAYS = config.get('Grocy', 'GROCY_DEFAULT_BEST_BEFORE_DAYS') 20 | GROCY_LOCATION = {} 21 | for key in config['GrocyLocation']: 22 | GROCY_LOCATION[key] = config.get('GrocyLocation', key) 23 | X_RapidAPI_Key = config.get('RapidAPI', 'X_RapidAPI_Key') 24 | 25 | app = Flask(__name__) 26 | grocy = Grocy(GROCY_URL, GROCY_API, GROCY_PORT, verify_ssl = True) 27 | 28 | def add_product(dict_good, client): 29 | good_name = "" 30 | if "description" in dict_good: 31 | good_name = dict_good["description"] 32 | elif "description_cn" in dict_good: 33 | good_name = dict_good["description_cn"] 34 | if not good_name: 35 | return False 36 | 37 | data_grocy = { 38 | "name": good_name, 39 | "description": "", 40 | "location_id": GROCY_LOCATION[client], 41 | "qu_id_purchase": GROCY_DEFAULT_QUANTITY_UNIT_ID, 42 | "qu_id_stock": GROCY_DEFAULT_QUANTITY_UNIT_ID, 43 | "default_best_before_days": GROCY_DEFAULT_BEST_BEFORE_DAYS, 44 | "default_consume_location_id": GROCY_LOCATION[client], 45 | "move_on_open": "1" 46 | } 47 | 48 | if ("gpc" in dict_good) and dict_good["gpc"]: 49 | best_before_days = gpc_best_before_days(int(dict_good["gpc"])) 50 | if best_before_days: 51 | data_grocy["default_best_before_days"] = best_before_days 52 | 53 | # add product 54 | response_grocy = grocy.add_generic(EntityType.PRODUCTS, data_grocy) 55 | 56 | # # add gds info 57 | grocy.set_userfields( 58 | EntityType.PRODUCTS, 59 | int(response_grocy["created_object_id"]), 60 | "GDSInfo", 61 | json.dumps(dict_good, ensure_ascii=False) 62 | ) 63 | 64 | # add barcode, ex. 06921168593910 65 | data_barcode = { 66 | "product_id": int(response_grocy["created_object_id"]), 67 | "barcode": dict_good["gtin"] 68 | } 69 | grocy.add_generic(EntityType.PRODUCT_BARCODES, data_barcode) 70 | # add barcode, EAN-13, ex. 6921168593910 71 | if dict_good["gtin"].startswith("0"): 72 | data_barcode = { 73 | "product_id": int(response_grocy["created_object_id"]), 74 | "barcode": dict_good["gtin"].strip("0") 75 | } 76 | grocy.add_generic(EntityType.PRODUCT_BARCODES, data_barcode) 77 | 78 | # add picture 79 | pic_url = "" 80 | if ("picfilename" in dict_good) and dict_good['picfilename']: 81 | pic_url = dict_good["picfilename"] 82 | elif ("picture_filename" in dict_good) and dict_good['picture_filename']: 83 | pic_url = dict_good["picture_filename"] 84 | 85 | if pic_url: 86 | try: 87 | response_img = requests.get(pic_url,{'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"}) 88 | if response_img.status_code == 200: 89 | image_data = response_img.content 90 | with open("img.png", 'wb') as o: 91 | output_data = remove(image_data) 92 | o.write(output_data) 93 | grocy.add_product_pic(int(response_grocy["created_object_id"]),"img.png") 94 | except requests.exceptions.RequestException as err: 95 | print("Request error:", err) 96 | 97 | grocy.add_product_by_barcode(dict_good["gtin"],1.0,0.0) 98 | return True 99 | 100 | def gpc_best_before_days(Code): 101 | with open('gpc_brick_code.json') as json_file: 102 | gpc_data = json.load(json_file) 103 | 104 | best_before_days = {} 105 | best_before_days["7"] = [50370000, 50380000, 50350000,] 106 | best_before_days["14"] = [50250000, 10000025, 10006970, 10000278, 10006979, ] 107 | best_before_days["152"] = [50270000, 50310000,] 108 | best_before_days["305"] = [94000000, 50000000, 10120000, 10110000,] 109 | best_before_days["670"] = [] 110 | best_before_days["1005"] = [53000000, 47100000, 47190000, 51000000, 10100000,] 111 | 112 | for item in gpc_data["Schema"]: 113 | if item["Code"] == Code: 114 | codes = [ 115 | item["Code"], 116 | item["Code-1"], 117 | item["Code-2"], 118 | item["Code-3"] 119 | ] 120 | for day, filter_codes in best_before_days.items(): 121 | if any(code in filter_codes for code in codes): 122 | return day 123 | 124 | @app.route('/') 125 | def index(): 126 | return render_template("index.html") 127 | 128 | @app.route('/add', methods=['POST']) 129 | def add(): 130 | data = request.json 131 | client = data.get("client", "") 132 | aimid = data.get("aimid", "") 133 | barcode = data.get("barcode", "") 134 | 135 | try: 136 | grocy.product_by_barcode(barcode) 137 | grocy.add_product_by_barcode(barcode,1.0,0.0) 138 | 139 | response_data = {"message": "Item added successfully"} 140 | return jsonify(response_data), 200 141 | except: 142 | if aimid == "]E0": 143 | spider = BarCodeSpider(rapid_api_url="https://barcodes1.p.rapidapi.com/", 144 | x_rapidapi_key=X_RapidAPI_Key, 145 | x_rapidapi_host="barcodes1.p.rapidapi.com") 146 | good = spider.get_good(barcode) 147 | if add_product(good, client): 148 | response_data = {"message": "New item added successfully"} 149 | return jsonify(response_data), 200 150 | else: 151 | response_data = {"message": "Fail to add new item"} 152 | return jsonify(response_data), 400 153 | else: 154 | response_data = {"message": "Unsupport barcode"} 155 | return jsonify(response_data), 400 156 | 157 | @app.route('/consume', methods=['POST']) 158 | def consume(): 159 | try: 160 | data = request.json 161 | barcode = data.get("barcode", "") 162 | grocy.consume_product_by_barcode(barcode) 163 | response_data = {"message": "Item removed successfully"} 164 | return jsonify(response_data), 200 165 | except Exception as e: 166 | error_message = str(e) 167 | response_data = {"error": error_message} 168 | return jsonify(response_data), 400 169 | 170 | if __name__ == '__main__': 171 | app.run(host='0.0.0.0', port=9288) 172 | -------------------------------------------------------------------------------- /config_example.ini: -------------------------------------------------------------------------------- 1 | [Grocy] 2 | GROCY_URL = https://example.com 3 | GROCY_PORT = 443 4 | GROCY_API = YOUR_GROCY_API_KEY 5 | # GROCY_DEFAULT_QUANTITY_UNIT_ID 在 shell内获取: 6 | ; curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/quantity_units' \ -H 'accept: application/json' \ -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ | echo -e "$(cat)" 7 | GROCY_DEFAULT_QUANTITY_UNIT_ID = 1 # 默认的数量单位ID 8 | GROCY_DEFAULT_BEST_BEFORE_DAYS = 365 # 默认的保质期天数 9 | 10 | # 存储位置ID,与scanner.ino内的位置名称对应 11 | # shell内获取,替换以下地址\端口\api_key: 12 | ; curl -X 'GET' 'https://EXAMPLE.COM:PORT/api/objects/locations' \ 13 | ; -H 'accept: application/json' \ 14 | ; -H 'GROCY-API-KEY:YOUR_GROCY_API_KEY' \ 15 | ; | echo -e "$(cat)" 16 | [GrocyLocation] 17 | pantry = 1 18 | temporary_storage = 2 19 | fridge = 3 20 | living_room = 4 21 | bedroom = 5 22 | bathroom = 6 23 | 24 | [RapidAPI] 25 | X_RapidAPI_Key = YOUR_RapidAPI_API_KEY 26 | 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | spider: 4 | image: osnsyc/grocycompanioncn:latest 5 | restart: always 6 | ports: 7 | - "9288:9288" 8 | volumes: 9 | - ./config.ini:/usr/src/app/config.ini 10 | # - ./u2net.onnx:/root/.u2net/u2net.onnx 11 | networks: 12 | - grocy_cn_campanion 13 | 14 | networks: 15 | grocy_cn_campanion: 16 | -------------------------------------------------------------------------------- /document/GM805条码识别模块用户手册V1.2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/GM805条码识别模块用户手册V1.2.pdf -------------------------------------------------------------------------------- /document/hardware-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/hardware-connection.png -------------------------------------------------------------------------------- /document/scanner-setting-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/scanner-setting-1.png -------------------------------------------------------------------------------- /document/scanner-setting-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/scanner-setting-2.png -------------------------------------------------------------------------------- /document/scanner-setting-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/scanner-setting-3.png -------------------------------------------------------------------------------- /document/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/document/shell.png -------------------------------------------------------------------------------- /esp8266/BarcodeScanner.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define SERVER "http://YOUR_SEVER_IP:9288" //GrocyCompanionCN api的地址 5 | #define CLIENT "temporary_storage" //对应config.ini中GrocyLocation的值 6 | #define GPIO0_PIN 0 7 | #define LED_PIN 2 8 | #define HTTP_CODE_OK 200 9 | #ifndef STASSID 10 | #define STASSID "YOUR_SSID" //WiFi名 11 | #define STAPSK "YOUR_PASSWORD" //WiFi密码 12 | #endif 13 | 14 | void setup() { 15 | Serial.begin(9600); 16 | WiFi.begin(STASSID, STAPSK); 17 | pinMode(GPIO0_PIN, INPUT); 18 | pinMode(LED_PIN, OUTPUT); 19 | digitalWrite(LED_PIN, LOW); 20 | } 21 | 22 | void loop() { 23 | // Wait for WiFi connection 24 | if ((WiFi.status() == WL_CONNECTED)) { 25 | digitalWrite(LED_PIN, HIGH); 26 | if (Serial.available()) { 27 | 28 | String serialString = Serial.readStringUntil('\n'); // Read a line from Serial 29 | // Remove leading and trailing whitespace, including newline characters 30 | serialString.trim(); 31 | 32 | // Ensure serialString has at least 4 characters 33 | if (serialString.length() >= 4) { 34 | String code = serialString.substring(0, 3); // Get the first 3 characters 35 | String digits = serialString.substring(3); // Get the rest of the string 36 | int gpio0State = digitalRead(GPIO0_PIN); 37 | requestPost(code, digits, gpio0State); 38 | } else { 39 | errorBlink(); 40 | } 41 | } 42 | } else { 43 | } 44 | } 45 | 46 | void requestPost(String code, String digits, int gpio0State) { 47 | WiFiClient client; 48 | HTTPClient http; 49 | 50 | int httpCode = 0; 51 | if (gpio0State == 1){ 52 | http.begin(client, String(SERVER) + "/consume"); 53 | http.addHeader("Content-Type", "application/json"); 54 | httpCode = http.POST("{\"client\":\"" CLIENT "\",\"aimid\":\"" + code + "\",\"barcode\":\"" + digits + "\"}"); 55 | } else { 56 | http.begin(client, String(SERVER) + "/add"); 57 | http.addHeader("Content-Type", "application/json"); 58 | httpCode = http.POST("{\"client\":\"" CLIENT "\",\"aimid\":\"" + code + "\",\"barcode\":\"" + digits + "\"}"); 59 | } 60 | 61 | if (httpCode == HTTP_CODE_OK) { 62 | successBlink(); 63 | } else { 64 | errorBlink(); 65 | } 66 | http.end(); 67 | } 68 | 69 | void successBlink(){ 70 | digitalWrite(LED_PIN, LOW); 71 | delay(500); 72 | digitalWrite(LED_PIN, HIGH); 73 | } 74 | 75 | void errorBlink(){ 76 | for(int i=0;i<=10;i++){ 77 | digitalWrite(LED_PIN, LOW); 78 | delay(100); 79 | digitalWrite(LED_PIN, HIGH); 80 | delay(100); 81 | } 82 | } -------------------------------------------------------------------------------- /esp8266/back.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/esp8266/back.stl -------------------------------------------------------------------------------- /esp8266/base.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/esp8266/base.stl -------------------------------------------------------------------------------- /esp8266/body.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osnsyc/GrocyCompanionCN/3e1ace40a92984acb4fb36e5c059cb6bf5a56dae/esp8266/body.stl -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygrocy==2.0.0 2 | Requests==2.31.0 3 | rembg==2.0.50 4 | Flask==2.0.3 5 | Werkzeug==2.0.3 6 | -------------------------------------------------------------------------------- /spider/__init__.py: -------------------------------------------------------------------------------- 1 | from spider.barcode_spider import * 2 | -------------------------------------------------------------------------------- /spider/barcode_spider.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # coding = utf-8 3 | import requests 4 | import logging 5 | import json 6 | 7 | # import time 8 | # from pyvirtualdisplay import Display 9 | # from DrissionPage.easy_set import set_paths, set_headless 10 | # from DrissionPage import ChromiumPage, ChromiumOptions 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | class BarCodeSpider: 15 | ''' 16 | 条形码爬虫类 17 | ''' 18 | def __init__(self, rapid_api_url="https://barcodes1.p.rapidapi.com/", 19 | x_rapidapi_key="", 20 | x_rapidapi_host="barcodes1.p.rapidapi.com"): 21 | 22 | self.logger = logging.getLogger(__name__) 23 | self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" 24 | self.base_url = 'https://bff.gds.org.cn/gds/searching-api/ProductService/homepagestatistic' 25 | self.domestic_url = "https://bff.gds.org.cn/gds/searching-api/ProductService/ProductListByGTIN" 26 | self.domestic_url_simple = "https://bff.gds.org.cn/gds/searching-api/ProductService/ProductSimpleInfoByGTIN" 27 | self.imported_url = "https://bff.gds.org.cn/gds/searching-api/ImportProduct/GetImportProductDataForGtin" 28 | self.imported_url_blk = "https://www.barcodelookup.com/" 29 | self.rapid_api_url = rapid_api_url 30 | self.x_rapidapi_key = x_rapidapi_key 31 | self.x_rapidapi_host= x_rapidapi_host 32 | 33 | def get_domestic_good(self, barcode): 34 | session = requests.session() 35 | session.headers.update({'User-Agent': self.user_agent}) 36 | response = session.get(self.base_url) 37 | if response.status_code != 200: 38 | self.logger.error( 39 | "error in getting base_url status_code is {}, barcode is {}".format( 40 | response.status_code)) 41 | return None 42 | 43 | payload = {'PageSize': '30', 'PageIndex': '1', 'SearchItem': str(barcode)} 44 | response_domestic_url = session.get(self.domestic_url, params=payload) 45 | if response_domestic_url.status_code != 200: 46 | self.logger.error( 47 | "error in getting domestic_url status_code is {}, barcode is {}".format( 48 | response_domestic_url.status_code)) 49 | return None 50 | 51 | good = json.loads(response_domestic_url.text) 52 | if good["Code"] == 2: 53 | self.logger.error("error, {}, barcode is {}".format(good["Msg"], barcode)) 54 | return None 55 | if good["Code"] != 1 or good["Data"]["Items"] == []: 56 | self.logger.error("error, item no found, barcode is {}".format(barcode)) 57 | return None 58 | 59 | base_id = good["Data"]["Items"][0]["base_id"] 60 | payload = {'gtin': str(barcode), 'id': base_id} 61 | response_domestic_url_simple = session.get(self.domestic_url_simple, params=payload) 62 | if response_domestic_url_simple.status_code != 200: 63 | return self.rework_good(good["Data"]["Items"][0]) 64 | 65 | simpleInfo = json.loads(response_domestic_url_simple.text) 66 | if simpleInfo["Code"] != 1: 67 | return self.rework_good(good["Data"]["Items"][0]) 68 | if simpleInfo["Data"] != "": 69 | good["Data"]["Items"][0]["simple_info"] = simpleInfo["Data"] 70 | return self.rework_good(good["Data"]["Items"][0]) 71 | 72 | return self.rework_good(good["Data"]["Items"][0]) 73 | 74 | def get_imported_good(self, barcode): 75 | session = requests.session() 76 | session.headers.update({'User-Agent': self.user_agent}) 77 | response = session.get(self.base_url) 78 | if response.status_code != 200: 79 | self.logger.error( 80 | "error in getting base_url status_code is {}, barcode is {}".format( 81 | response.status_code, barcode)) 82 | good_blk = self.get_imorted_good_from_blk(barcode) 83 | return good_blk 84 | 85 | payload = {'PageSize': '30', 'PageIndex': '1', 'Gtin': str(barcode), "Description": "", "AndOr": "0"} 86 | response_imported_url = session.get(self.imported_url, params=payload) 87 | if response_imported_url.status_code != 200: 88 | self.logger.error( 89 | "error in getting imported_url status_code is {}, barcode is {}".format( 90 | response_imported_url.status_code, barcode)) 91 | good_blk = self.get_imorted_good_from_blk(barcode) 92 | return good_blk 93 | 94 | good = json.loads(response_imported_url.text) 95 | if good["Code"] != 1 or good["Data"]["Items"] == []: 96 | self.logger.error("error, item no found, barcode is {}".format(barcode)) 97 | good_blk = self.get_imorted_good_from_blk(barcode) 98 | return good_blk 99 | 100 | if (len(good["Data"]["Items"]) == 1) and (good["Data"]["Items"][0]["description_cn"] != None): 101 | return self.rework_good(good["Data"]["Items"][0]) 102 | 103 | if (len(good["Data"]["Items"]) == 1) and (good["Data"]["Items"][0]["description_cn"] == None): 104 | good_blk = self.get_imorted_good_from_blk(barcode) 105 | return good_blk 106 | 107 | if len(good["Data"]["Items"]) >= 2: 108 | for item in good["Data"]["Items"]: 109 | if item["realname"] == item["importer_name"]: 110 | return self.rework_good(item) 111 | return self.rework_good(good["Data"]["Items"][0]) 112 | 113 | def get_imorted_good_from_blk(self, barcode): 114 | good = {} 115 | querystring = {"query": barcode} 116 | headers = { 117 | "X-RapidAPI-Key": self.x_rapidapi_key, 118 | "X-RapidAPI-Host": self.x_rapidapi_host 119 | } 120 | response = requests.get(self.rapid_api_url, headers=headers, params=querystring) 121 | good_dict = response.json() 122 | if "product" not in good_dict: 123 | return None 124 | 125 | good["description_cn"] = good_dict["product"]["title"] 126 | good["picfilename"] = good_dict["product"]["images"][0] 127 | attributes = good_dict["product"]["attributes"] 128 | good["specification_cn"] = ", ".join([f"{key}:{value}" for key, value in attributes.items()]) 129 | good["gtin"] = barcode 130 | 131 | return good 132 | 133 | ''' 134 | # Drissionpage method 135 | def get_imorted_good_from_blk(self, barcode): 136 | good = {} 137 | 138 | display = Display(visible=0, size=(1920, 1080)) 139 | display.start() 140 | 141 | set_headless(False) 142 | set_paths(browser_path='/usr/bin/google-chrome') 143 | page = ChromiumPage() 144 | page.get(self.imported_url_blk + str(barcode)) 145 | time.sleep(6) 146 | page.get_screenshot(path='page.png', full_page=True) 147 | if ("Bad Barcode" in page.title) or ("Not Found" in page.title): 148 | print(page.title) 149 | else: 150 | if "no-image" not in page.ele("xpath://div[@id='largeProductImage']/img").attr("src"): 151 | good["picfilename"] = page.ele("xpath://div[@id='largeProductImage']/img").attr("src") 152 | good["description_cn"] = page.ele("xpath://div[@id='largeProductImage']/img").attr("alt") 153 | good["specification_cn"] = "" 154 | specs = page.eles("xpath://ul[@id='product-attributes']/li[@class='product-text']/span") 155 | for spec in specs: 156 | good["specification_cn"] = good["specification_cn"] + spec.text + ", " 157 | good["gtin"] = barcode 158 | 159 | page.quit() 160 | display.stop() 161 | return good 162 | ''' 163 | 164 | def rework_good(self, good): 165 | if "id" in good: 166 | del good["id"] 167 | if "f_id" in good: 168 | del good["f_id"] 169 | if "brandid" in good: 170 | del good["brandid"] 171 | if "base_id" in good: 172 | del good["base_id"] 173 | 174 | if good["branch_code"]: 175 | good["branch_code"] = good["branch_code"].strip() 176 | if "picture_filename" in good: 177 | if good["picture_filename"] and (not good["picture_filename"].startswith("http")): 178 | good["picture_filename"] = "https://oss.gds.org.cn" + good["picture_filename"] 179 | if "picfilename" in good: 180 | if good["picfilename"] and (not good["picfilename"].startswith("http")): 181 | good["picfilename"] = "https://oss.gds.org.cn" + good["picfilename"] 182 | 183 | return good 184 | 185 | def get_good(self, barcode): 186 | if barcode.startswith("69") or barcode.startswith("069"): 187 | return self.get_domestic_good(barcode) 188 | else: 189 | return self.get_imported_good(barcode) 190 | 191 | def main(): 192 | #国产商品 193 | # good = BarCodeSpider.get_good('06917878036526') 194 | #进口商品 195 | # good = BarCodeSpider.get_good('4901201103803') 196 | #国际商品 197 | good = BarCodeSpider.get_good('3346476426843') 198 | 199 | print(good) 200 | 201 | if __name__ == '__main__': 202 | main() 203 | 204 | ''' 205 | 国产商品字典 206 | "keyword": "农夫山泉", 207 | "branch_code": "3301 ", 208 | "gtin": "06921168593910", 209 | "specification": "900毫升", 210 | "is_private": false, 211 | "firm_name": "农夫山泉股份有限公司", 212 | "brandcn": "农夫山泉", 213 | "picture_filename": "https://oss.gds.org.cn/userfile/uploada/gra/1712072230/06921168593910/06921168593910.1.jpg", 214 | "description": "农夫山泉NFC橙汁900ml", 215 | "logout_flag": "0", 216 | "have_ms_product": 0, 217 | "base_create_time": "2018-07-10T10:01:31.763Z", 218 | "branch_name": "浙江分中心", 219 | "base_source": "Source", 220 | "gpc": "10000201", 221 | "gpcname": "即饮型调味饮料", 222 | "saledate": "2017-11-30T16:00:00Z", 223 | "saledateyear": 2017, 224 | "base_last_updated": "2019-01-09T02:00:00Z", 225 | "base_user_id": "源数据服务", 226 | "code": "69211685", 227 | "levels": null, 228 | "levels_source": null, 229 | "valid_date": "2023-02-16T16:00:00Z", 230 | "logout_date": null, 231 | "gtinstatus": 1 232 | ''' 233 | 234 | ''' 235 | 进口商品字典 236 | "gtin": "04901201103803", 237 | "description_cn": "UCC117速溶综合咖啡90g", 238 | "specification_cn": "90克", 239 | "brand_cn": "悠诗诗", 240 | "gpc": "10000115", 241 | "gpc_name": "速溶咖啡", 242 | "origin_cn": "392", 243 | "origin_name": "日本", 244 | "codeNet": null, 245 | "codeNetContent": null, 246 | "suggested_retail_price": 0, 247 | "suggested_retail_price_unit": "人民币", 248 | "txtKeyword": null, 249 | "picfilename": "https://oss.gds.org.cn/userfile/importcpfile/201911301903478446204015916.png", 250 | "realname": "磨禾(厦门)进出口有限公司", 251 | "branch_code": "3501", 252 | "branch_name": "福建分中心", 253 | "importer_name": "磨禾(厦门)进出口有限公司", 254 | "certificatefilename": null, 255 | "certificatestatus": 0, 256 | "isprivary": 0, 257 | "isconfidentiality": 0, 258 | "datasource": 0 259 | ''' 260 | 261 | ''' 262 | 国际商品字典 263 | ''' -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrocyCNCompanion 6 | 7 | 8 |

GrocyCNCompanion Started!

9 | 10 | --------------------------------------------------------------------------------