diff --git a/changes.md b/changes.md index 8d8ee78..0b2d475 100644 --- a/changes.md +++ b/changes.md @@ -1,5 +1,9 @@ # Change Log +## v0.1.7 F2SR14 Support +* Support for F4SR14 added +* Update Button added + ## v0.1.6 Sensor Values are evaluated * Sensor values are displayed in command line * Sponsor button added diff --git a/eo_man/controller/app_bus.py b/eo_man/controller/app_bus.py index f2767ae..f2e51a1 100644 --- a/eo_man/controller/app_bus.py +++ b/eo_man/controller/app_bus.py @@ -17,6 +17,7 @@ class AppBusEventType(Enum): SET_DATA_TABLE_FILTER = 13 # applies data filter to data table ADDED_DATA_TABLE_FILTER = 14 # adds data filter to application data REMOVED_DATA_TABLE_FILTER = 15 # remove data filter from application data + RELOAD = 16 class AppBus(): diff --git a/eo_man/data/app_info.py b/eo_man/data/app_info.py index 21ff79f..2b145ab 100644 --- a/eo_man/data/app_info.py +++ b/eo_man/data/app_info.py @@ -1,8 +1,11 @@ import os +import requests class ApplicationInfo(): app_info:dict[str:str]=None + pypi_info_latest_versino:dict=None + pypi_info_current_version:dict=None @classmethod def get_app_info(cls, filename:str=None): @@ -39,9 +42,31 @@ def get_app_info(cls, filename:str=None): app_info['license'] = l.split(':',1)[1].strip() elif l.startswith('Requires-Python:'): app_info['requires-python'] = l.split(':',1)[1].strip() + + # get latest version + cls.pypi_info_latest_versino = cls._get_info_from_pypi() + app_info['lastest_available_version'] = cls.pypi_info_latest_versino.get('info', {}).get('version', None) + # get current/installed version + if app_info['version'] is not None and app_info['version'] != '': + cls.pypi_info_current_version = cls._get_info_from_pypi(app_info['version']) return app_info + @classmethod + def _get_info_from_pypi(cls, version:str=''): + if version is not None and version != '': + if not version.startswith('/') and not version.endswith('/'): + version = version + '/' + + url = f"https://pypi.org/pypi/eo-man/{version}json" + + response = requests.get(url) + if response.status_code == 200: + return response.json() + + return {} + + @classmethod def get_app_info_as_str(cls, separator:str='\n', prefix:str='') -> str: result = '' @@ -75,4 +100,12 @@ def get_license(cls) -> str: @classmethod def get_requires_python(cls) -> str: - return cls.get_app_info().get('requires-python', 'unknown') \ No newline at end of file + return cls.get_app_info().get('requires-python', 'unknown') + + @classmethod + def get_lastest_available_version(cls) -> str: + return cls.get_app_info().get('lastest_available_version', 'unknown') + + @classmethod + def is_version_up_to_date(cls) -> bool: + return cls.pypi_info_current_version['info']['version'] == cls.pypi_info_latest_versino['info']['version'] \ No newline at end of file diff --git a/eo_man/data/data_helper.py b/eo_man/data/data_helper.py index 3eef6d9..40c5018 100644 --- a/eo_man/data/data_helper.py +++ b/eo_man/data/data_helper.py @@ -34,6 +34,7 @@ {'hw-type': 'FSR14_1x', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'PCT14-function-group': 2, 'PCT14-key-function': 51, 'description': 'Eltako relay', 'address_count': 1}, {'hw-type': 'FSR14_x2', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'PCT14-function-group': 2, 'PCT14-key-function': 51, 'description': 'Eltako relay', 'address_count': 2}, {'hw-type': 'FSR14_4x', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'PCT14-function-group': 2, 'PCT14-key-function': 51, 'description': 'Eltako relay', 'address_count': 4}, + {'hw-type': 'F4SR14_LED', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'PCT14-function-group': 2, 'PCT14-key-function': 51, 'description': 'Eltako relay', 'address_count': 4}, {'hw-type': 'FSB14', CONF_EEP: 'G5-3F-7F', 'sender_eep': 'H5-3F-7F', CONF_TYPE: Platform.COVER, 'PCT14-function-group': 2, 'PCT14-key-function': 31, 'description': 'Eltako cover', 'address_count': 2}, diff --git a/eo_man/data/device.py b/eo_man/data/device.py index 53f6b97..ddc5de8 100644 --- a/eo_man/data/device.py +++ b/eo_man/data/device.py @@ -117,35 +117,40 @@ async def async_get_bus_device_by_natvice_bus_object(cls, device: BusObject, fam if bd.dev_size > 1: bd.name += f" ({bd.channel}/{bd.dev_size})" - info:dict = find_device_info_by_device_type(bd.device_type) + Device.set_suggest_ha_config(bd) + + return bd + + @classmethod + def set_suggest_ha_config(cls, device): + id = int.from_bytes( AddressExpression.parse(device.address)[0], 'big') + info:dict = find_device_info_by_device_type(device.device_type) if info is not None: - bd.use_in_ha = True - bd.ha_platform = info[CONF_TYPE] - bd.eep = info.get(CONF_EEP, None) + device.use_in_ha = True + device.ha_platform = info[CONF_TYPE] + device.eep = info.get(CONF_EEP, None) if info.get('sender_eep', None): - bd.additional_fields['sender'] = { + device.additional_fields['sender'] = { CONF_ID: a2s( SENDER_BASE_ID + id ), CONF_EEP: info.get('sender_eep') } if info[CONF_TYPE] == Platform.COVER: - bd.additional_fields[CONF_DEVICE_CLASS] = 'shutter' - bd.additional_fields[CONF_TIME_CLOSES] = 25 - bd.additional_fields[CONF_TIME_OPENS] = 25 + device.additional_fields[CONF_DEVICE_CLASS] = 'shutter' + device.additional_fields[CONF_TIME_CLOSES] = 25 + device.additional_fields[CONF_TIME_OPENS] = 25 if info[CONF_TYPE] == Platform.CLIMATE: - bd.additional_fields[CONF_TEMPERATURE_UNIT] = f"'{UnitOfTemperature.KELVIN}'" - bd.additional_fields[CONF_MIN_TARGET_TEMPERATURE] = 16 - bd.additional_fields[CONF_MAX_TARGET_TEMPERATURE] = 25 - thermostat = BusObjectHelper.find_sensor(bd.memory_entries, device.address, channel, in_func_group=1) + device.additional_fields[CONF_TEMPERATURE_UNIT] = f"'{UnitOfTemperature.KELVIN}'" + device.additional_fields[CONF_MIN_TARGET_TEMPERATURE] = 16 + device.additional_fields[CONF_MAX_TARGET_TEMPERATURE] = 25 + thermostat = BusObjectHelper.find_sensor(device.memory_entries, device.address, device.channel, in_func_group=1) if thermostat: - bd.additional_fields[CONF_ROOM_THERMOSTAT] = {} - bd.additional_fields[CONF_ROOM_THERMOSTAT][CONF_ID] = b2s(thermostat.sensor_id) - bd.additional_fields[CONF_ROOM_THERMOSTAT][CONF_EEP] = A5_10_06.eep_string #TODO: derive EEP from switch/sensor function + device.additional_fields[CONF_ROOM_THERMOSTAT] = {} + device.additional_fields[CONF_ROOM_THERMOSTAT][CONF_ID] = b2s(thermostat.sensor_id) + device.additional_fields[CONF_ROOM_THERMOSTAT][CONF_EEP] = A5_10_06.eep_string #TODO: derive EEP from switch/sensor function #TODO: cooling_mode - - return bd @classmethod def get_decentralized_device_by_telegram(cls, msg: RPSMessage): diff --git a/eo_man/data/ha_config_generator.py b/eo_man/data/ha_config_generator.py index 934f3a4..738777e 100644 --- a/eo_man/data/ha_config_generator.py +++ b/eo_man/data/ha_config_generator.py @@ -60,14 +60,15 @@ def generate_ha_config(self, device_list:list[Device]) -> str: out += f" {CONF_DEVICES}:\n" for platform in ha_platforms: - out += f" {platform}:\n" - for _d in [d for d in devices if d.ha_platform == platform]: - device:Device = _d - # devices - if device.base_id == fam14.external_id: - out += self.config_section_from_device_to_string(device, True, 0) + "\n\n" - elif 'sensor' in platform: - out += self.config_section_from_device_to_string(device, True, 0) + "\n\n" + if platform != '': + out += f" {platform}:\n" + for _d in [d for d in devices if d.ha_platform == platform]: + device:Device = _d + # devices + if device.base_id == fam14.external_id: + out += self.config_section_from_device_to_string(device, True, 0) + "\n\n" + elif 'sensor' in platform: + out += self.config_section_from_device_to_string(device, True, 0) + "\n\n" # logs out += "logger:\n" out += " default: info\n" diff --git a/eo_man/icons/Software-update-available.png b/eo_man/icons/Software-update-available.png new file mode 100644 index 0000000..d9e147f Binary files /dev/null and b/eo_man/icons/Software-update-available.png differ diff --git a/eo_man/icons/image_gallary.py b/eo_man/icons/image_gallary.py index 3e40dd4..ec7239d 100644 --- a/eo_man/icons/image_gallary.py +++ b/eo_man/icons/image_gallary.py @@ -1,6 +1,9 @@ import os from PIL import Image, ImageTk +# icon list +# https://commons.wikimedia.org/wiki/Comparison_of_icon_sets + class ImageGallery(): @classmethod @@ -44,4 +47,8 @@ def get_about_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: @classmethod def get_github_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: - return ImageGallery.get_image("github_icon.png", size) \ No newline at end of file + return ImageGallery.get_image("github_icon.png", size) + + @classmethod + def get_software_update_available_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: + return ImageGallery.get_image("Software-update-available.png", size) \ No newline at end of file diff --git a/eo_man/view/menu_presenter.py b/eo_man/view/menu_presenter.py index a5c3895..b4774f4 100644 --- a/eo_man/view/menu_presenter.py +++ b/eo_man/view/menu_presenter.py @@ -3,10 +3,12 @@ from tkinter import * from tkinter import filedialog import logging +from tkinter import messagebox import webbrowser from ..controller.app_bus import AppBus, AppBusEventType +from ..data.device import Device from ..data.data_manager import DataManager from ..data.ha_config_generator import HomeAssistantConfigurationGenerator @@ -72,6 +74,12 @@ def __init__(self, main: Tk, app_bus: AppBus, data_manager: DataManager): accelerator="ALT+F") + ha_menu = Menu(menu_bar, tearoff=False) + menu_bar.add_cascade(label="Home Assistant", menu=ha_menu) + ha_menu.add_command(label="Reset to suggested HA properties.", + command=self.reset_to_suggested_ha_properties) + + help_menu = Menu(menu_bar, tearoff=False) menu_bar.add_cascade(label="Help", menu=help_menu) @@ -224,8 +232,21 @@ def export_ha_config(self, save_as:bool=False): self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': msg, 'log-level': 'ERROR', 'color': 'red'}) logging.exception(msg, exc_info=True) + + def reset_to_suggested_ha_properties(self): + yes = messagebox.askyesno("Reset to suggested HA properties.", "Do you want to reset Home Assistant relevant properties of all devices to initial values?") + if yes: + for d in self.data_manager.devices.values(): + Device.set_suggest_ha_config(d) + if d.is_bus_device(): + self.app_bus.fire_event(AppBusEventType.UPDATE_DEVICE_REPRESENTATION, d) + else: + self.app_bus.fire_event(AppBusEventType.UPDATE_SENSOR_REPRESENTATION, d) + def open_eo_man_repo(self): webbrowser.open_new(r"https://github.com/grimmpp/enocean-device-manager") def open_eo_man_documentation(self): webbrowser.open_new(r"https://github.com/grimmpp/enocean-device-manager/tree/main/docs") + + \ No newline at end of file diff --git a/eo_man/view/tool_bar.py b/eo_man/view/tool_bar.py index d906209..555d5b3 100644 --- a/eo_man/view/tool_bar.py +++ b/eo_man/view/tool_bar.py @@ -1,13 +1,19 @@ +import threading import tkinter as tk import os from tkinter import * +from tkinter import messagebox from PIL import Image, ImageTk from idlelib.tooltip import Hovertip import webbrowser +import subprocess + +from eo_man import LOGGER from .donation_button import DonationButton from .menu_presenter import MenuPresenter from ..icons.image_gallary import ImageGallery +from ..data.app_info import ApplicationInfo as AppInfo class ToolBar(): @@ -32,10 +38,29 @@ def __init__(self, main: Tk, menu_presenter:MenuPresenter, row:int): b = self._create_img_button(f, "GitHub: EnOcean Device Manager Documentation", ImageGallery.get_help_icon(), menu_presenter.open_eo_man_documentation) b.pack(side=RIGHT, padx=(0,2), pady=2) + if not AppInfo.is_version_up_to_date(): + new_v = AppInfo.get_lastest_available_version() + b = self._create_img_button(f, f"New Software Version 'v{new_v}' is available.", ImageGallery.get_software_update_available_icon(), self.show_how_to_update) + b.pack(side=RIGHT, padx=(0,2), pady=2) + + def _create_img_button(self, f:Frame, tooltip:str, image:ImageTk.PhotoImage, command) -> Button: b = Button(f, image=image, relief=GROOVE, cursor="hand2", command=command) Hovertip(b,tooltip,300) b.image = image b.pack(side=LEFT, padx=(2,0), pady=2) return b - \ No newline at end of file + + def show_how_to_update(self): + base_path = os.environ.get('VIRTUAL_ENV', '') + if base_path != '': + base_path = os.path.join(base_path, 'Scripts') + + new_version = AppInfo.get_lastest_available_version() + + msg = f"A new version 'v{new_version}' of 'EnOcean Device Manager' is available. \n\n" + msg += f"You can update this application by entering \n" + msg += f"'{os.path.join(base_path, 'pip')}' install eo_man --upgrade'\n" + msg += f"into the command line." + + messagebox.showinfo("Update Available", msg) \ No newline at end of file diff --git a/setup.py b/setup.py index 4e32822..01b29a4 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ import os from setuptools import setup, find_packages from distutils.core import setup -import shutil -import re + base_dir = os.path.dirname(__file__) @@ -24,7 +23,7 @@ setup( name='eo_man', - version='0.1.6', + version='0.1.7', package_dir={'eo_man':"eo_man"}, # packages=find_packages("./eo-man"), #package_data={'': ['*.png']},