Skip to content

Commit

Permalink
Merge pull request #59 from custom-components/develop
Browse files Browse the repository at this point in the history
merge develop into master
  • Loading branch information
isabellaalstrom authored Aug 13, 2020
2 parents 1f3a044 + ebdb882 commit 5c0bdf0
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 147 deletions.
7 changes: 7 additions & 0 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
default_config:

logger:
default: error
logs:
custom_components.blueprint: debug

31 changes: 31 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "ludeeus/container:integration",
"context": "..",
"appPort": [
"9123:8123"
],
"postCreateCommand": "container install",
"runArgs": [
"-v",
"${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh"
],
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"tabnine.tabnine-vscode"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
}
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- uses: home-assistant/actions/hassfest@master
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@
3. Install Grocy
4. Restart Home Assistant
5. Go to Grocy-Wrench icon-Manage API keys-Add
6. Copy resulting API key and Grocy URL and input this in configuration.yaml:
7. Choose:
- Add `grocy:` to your HA configuration.
- In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Grocy"

8. Look for the new Grocy sensor in States and use its info
6. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Grocy"
7. Look for the new Grocy sensor in States and use its info


## Additional installation instructions for Hass.io users
Expand All @@ -26,9 +22,7 @@ The configuration is slightly different for users that use Hass.io and the [offi
5. Copy resulting API key
4. Go to Community > Store > Grocy
5. Install the Grocy integration component
6. Enter the previous API key and your URL and port number for your Grocy instance
7. Restart Home Assistant
8. Look for the new Grocy sensor in States and use its info

---
[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/X8X1LYUK)
6. Restart Home Assistant
7. Go to Grocy > Wrench icon > Manage API keys > Add
8. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Grocy"
9. Look for the new Grocy sensor in States and use its info
165 changes: 101 additions & 64 deletions custom_components/grocy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
The integration for grocy.
"""
import asyncio
import hashlib
import logging
import os
Expand All @@ -9,19 +10,33 @@
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (CONF_API_KEY, CONF_PORT, CONF_URL,
CONF_VERIFY_SSL)
from homeassistant.const import CONF_API_KEY, CONF_PORT, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.helpers import discovery, entity_component
from homeassistant.util import Throttle
from integrationhelper.const import CC_STARTUP_VERSION

from .const import (CHORES_NAME, CONF_BINARY_SENSOR, CONF_ENABLED, CONF_NAME,
CONF_SENSOR, DEFAULT_NAME, DEFAULT_PORT_NUMBER, DOMAIN,
DOMAIN_DATA, EXPIRED_PRODUCTS_NAME, EXPIRING_PRODUCTS_NAME,
ISSUE_URL, MISSING_PRODUCTS_NAME, PLATFORMS,
REQUIRED_FILES, SHOPPING_LIST_NAME, STARTUP, STOCK_NAME,
VERSION)
from .const import (
CHORES_NAME,
CONF_BINARY_SENSOR,
CONF_ENABLED,
CONF_NAME,
CONF_SENSOR,
DEFAULT_NAME,
DEFAULT_PORT_NUMBER,
DOMAIN,
DOMAIN_DATA,
EXPIRED_PRODUCTS_NAME,
EXPIRING_PRODUCTS_NAME,
ISSUE_URL,
MISSING_PRODUCTS_NAME,
PLATFORMS,
REQUIRED_FILES,
SHOPPING_LIST_NAME,
STARTUP,
STOCK_NAME,
VERSION,
)

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)

Expand Down Expand Up @@ -49,9 +64,7 @@
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT_NUMBER): cv.port,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
vol.Optional(CONF_SENSOR): vol.All(
cv.ensure_list, [SENSOR_SCHEMA]
),
vol.Optional(CONF_SENSOR): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
vol.Optional(CONF_BINARY_SENSOR): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
),
Expand All @@ -66,6 +79,7 @@ async def async_setup(hass, config):
"""Set up this component."""
return True


async def async_setup_entry(hass, config_entry):
"""Set up this integration using UI."""
from pygrocy import Grocy, TransactionType
Expand All @@ -86,8 +100,7 @@ async def async_setup_entry(hass, config_entry):
)

# Check that all required files are present
file_check = await check_files(hass)
if not file_check:
if not await hass.async_add_executor_job(check_files, hass):
return False

# Create DATA dict
Expand All @@ -98,7 +111,7 @@ async def async_setup_entry(hass, config_entry):
api_key = config_entry.data.get(CONF_API_KEY)
verify_ssl = config_entry.data.get(CONF_VERIFY_SSL)
port_number = config_entry.data.get(CONF_PORT)
hash_key = hashlib.md5(api_key.encode('utf-8') + url.encode('utf-8')).hexdigest()
hash_key = hashlib.md5(api_key.encode("utf-8") + url.encode("utf-8")).hexdigest()

# Configure the client.
grocy = Grocy(url, api_key, port_number, verify_ssl)
Expand All @@ -116,74 +129,79 @@ async def async_setup_entry(hass, config_entry):

@callback
def handle_add_product(call):
product_id = call.data['product_id']
amount = call.data.get('amount', 0)
price = call.data.get('price', None)
product_id = call.data["product_id"]
amount = call.data.get("amount", 0)
price = call.data.get("price", None)
grocy.add_product(product_id, amount, price)

hass.services.async_register(DOMAIN, "add_product", handle_add_product)

@callback
def handle_consume_product(call):
product_id = call.data['product_id']
amount = call.data.get('amount', 0)
spoiled = call.data.get('spoiled', False)
product_id = call.data["product_id"]
amount = call.data.get("amount", 0)
spoiled = call.data.get("spoiled", False)

transaction_type_raw = call.data.get('transaction_type', None)
transaction_type_raw = call.data.get("transaction_type", None)
transaction_type = TransactionType.CONSUME

if transaction_type_raw is not None:
transaction_type = TransactionType[transaction_type_raw]
grocy.consume_product(
product_id, amount,
spoiled=spoiled,
transaction_type=transaction_type)
product_id, amount, spoiled=spoiled, transaction_type=transaction_type
)

hass.services.async_register(
DOMAIN, "consume_product",
handle_consume_product)
hass.services.async_register(DOMAIN, "consume_product", handle_consume_product)

@callback
def handle_execute_chore(call):
chore_id = call.data['chore_id']
done_by = call.data.get('done_by', None)
tracked_time_str = call.data.get('tracked_time', None)
chore_id = call.data["chore_id"]
done_by = call.data.get("done_by", None)
tracked_time_str = call.data.get("tracked_time", None)

tracked_time = datetime.now()
if tracked_time_str is not None:
tracked_time = iso8601.parse_date(tracked_time_str)
grocy.execute_chore(chore_id, done_by, tracked_time)
asyncio.run_coroutine_threadsafe(
entity_component.async_update_entity(hass, "sensor.grocy_chores"), hass.loop
)

hass.services.async_register(DOMAIN, "execute_chore", handle_execute_chore)

return True


class GrocyData:
"""This class handle communication and stores the data."""

def __init__(self, hass, client):
"""Initialize the class."""
self.hass = hass
self.client = client
self.sensor_types_dict = { STOCK_NAME : self.async_update_stock,
CHORES_NAME : self.async_update_chores,
SHOPPING_LIST_NAME : self.async_update_shopping_list,
EXPIRING_PRODUCTS_NAME : self.async_update_expiring_products,
EXPIRED_PRODUCTS_NAME : self.async_update_expired_products,
MISSING_PRODUCTS_NAME : self.async_update_missing_products,
self.sensor_types_dict = {
STOCK_NAME: self.async_update_stock,
CHORES_NAME: self.async_update_chores,
SHOPPING_LIST_NAME: self.async_update_shopping_list,
EXPIRING_PRODUCTS_NAME: self.async_update_expiring_products,
EXPIRED_PRODUCTS_NAME: self.async_update_expired_products,
MISSING_PRODUCTS_NAME: self.async_update_missing_products,
}
self.sensor_update_dict = { STOCK_NAME : None,
CHORES_NAME : None,
SHOPPING_LIST_NAME : None,
EXPIRING_PRODUCTS_NAME : None,
EXPIRED_PRODUCTS_NAME : None,
MISSING_PRODUCTS_NAME : None,
self.sensor_update_dict = {
STOCK_NAME: None,
CHORES_NAME: None,
SHOPPING_LIST_NAME: None,
EXPIRING_PRODUCTS_NAME: None,
EXPIRED_PRODUCTS_NAME: None,
MISSING_PRODUCTS_NAME: None,
}

async def async_update_data(self, sensor_type):
"""Update data."""
sensor_update = self.sensor_update_dict[sensor_type]
db_changed = await self.hass.async_add_executor_job(self.client.get_last_db_changed)
sensor_update = self.sensor_update_dict[sensor_type]
db_changed = await self.hass.async_add_executor_job(
self.client.get_last_db_changed
)
if db_changed != sensor_update:
self.sensor_update_dict[sensor_type] = db_changed
if sensor_type in self.sensor_types_dict:
Expand All @@ -194,49 +212,65 @@ async def async_update_data(self, sensor_type):
async def async_update_stock(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][STOCK_NAME] = (
await self.hass.async_add_executor_job(self.client.stock, [True]))
self.hass.data[DOMAIN_DATA][
STOCK_NAME
] = await self.hass.async_add_executor_job(self.client.stock)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update_chores(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][CHORES_NAME] = (
await self.hass.async_add_executor_job(self.client.chores, [True]))
def wrapper():
return self.client.chores(True)

self.hass.data[DOMAIN_DATA][
CHORES_NAME
] = await self.hass.async_add_executor_job(wrapper)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update_shopping_list(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][SHOPPING_LIST_NAME] = (
await self.hass.async_add_executor_job(self.client.shopping_list, [True]))
def wrapper():
return self.client.shopping_list(True)

self.hass.data[DOMAIN_DATA][
SHOPPING_LIST_NAME
] = await self.hass.async_add_executor_job(wrapper)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update_expiring_products(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][EXPIRING_PRODUCTS_NAME] = (
await self.hass.async_add_executor_job(
self.client.expiring_products, [True]))
def wrapper():
return self.client.expiring_products(True)

self.hass.data[DOMAIN_DATA][
EXPIRING_PRODUCTS_NAME
] = await self.hass.async_add_executor_job(wrapper)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update_expired_products(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][EXPIRED_PRODUCTS_NAME] = (
await self.hass.async_add_executor_job(
self.client.expired_products, [True]))
def wrapper():
return self.client.expired_products(True)

self.hass.data[DOMAIN_DATA][
EXPIRED_PRODUCTS_NAME
] = await self.hass.async_add_executor_job(wrapper)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update_missing_products(self):
"""Update data."""
# This is where the main logic to update platform data goes.
self.hass.data[DOMAIN_DATA][MISSING_PRODUCTS_NAME] = (
await self.hass.async_add_executor_job(
self.client.missing_products, [True]))
def wrapper():
return self.client.missing_products(True)

self.hass.data[DOMAIN_DATA][
MISSING_PRODUCTS_NAME
] = await self.hass.async_add_executor_job(wrapper)


async def check_files(hass):
def check_files(hass):
"""Return bool that indicates if all files are present."""
# Verify that the user downloaded all files.
base = "{}/custom_components/{}/".format(hass.config.path(), DOMAIN)
Expand All @@ -254,6 +288,7 @@ async def check_files(hass):

return returnvalue


async def async_remove_entry(hass, config_entry):
"""Handle removal of an entry."""
try:
Expand All @@ -263,7 +298,9 @@ async def async_remove_entry(hass, config_entry):
_LOGGER.exception(error)
pass
try:
await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor")
await hass.config_entries.async_forward_entry_unload(
config_entry, "binary_sensor"
)
_LOGGER.info("Successfully removed sensor from the grocy integration")
except ValueError as error:
_LOGGER.exception(error)
Expand Down
Loading

0 comments on commit 5c0bdf0

Please sign in to comment.