diff --git a/broadlinkmanager/VERSION b/broadlinkmanager/VERSION index 8a30e8f..f4965a3 100755 --- a/broadlinkmanager/VERSION +++ b/broadlinkmanager/VERSION @@ -1 +1 @@ -5.4.0 +6.0.0 \ No newline at end of file diff --git a/broadlinkmanager/broadlinkmanager.py b/broadlinkmanager/broadlinkmanager.py index 4c01c9c..ed0f77f 100644 --- a/broadlinkmanager/broadlinkmanager.py +++ b/broadlinkmanager/broadlinkmanager.py @@ -1,19 +1,16 @@ # region Importing import os -import json import subprocess import time import broadlink import argparse -import datetime import re -import shutil import uvicorn -import socket -import aiofiles +from code import Code from os import environ, path from json import dumps +from sqliteconnector import SqliteConnector from broadlink.exceptions import ReadError, StorageError from broadlink import exceptions as e from broadlink.const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT @@ -38,13 +35,15 @@ from fastapi.encoders import jsonable_encoder from starlette_exporter import PrometheusMiddleware, handle_metrics + # Use to disable Google analytics code ENABLE_GOOGLE_ANALYTICS = os.getenv("ENABLE_GOOGLE_ANALYTICS") # endregion ip_format_regex = r"\b(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))\b" - +db = SqliteConnector() +db.create_tables() logger.info("OS: " + os.name) def validate_ip(ip): @@ -430,6 +429,11 @@ def about(request: Request): return templates.TemplateResponse('about.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) +@app.get('/saved', include_in_schema=False) +def about(request: Request): + return templates.TemplateResponse('saved.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) + + @app.get('/temperature', tags=["Commands"], summary="Read Temperature") def temperature(request: Request, mac: str = "", host: str = "", type: str = ""): logger.info("Getting temperature for device: " + host) @@ -460,10 +464,10 @@ def learnir(request: Request, mac: str = "", host: str = "", type: str = "", com break else: logger.error("No IR Data") - return JSONResponse('{"data":"","success":0,"message":"No Data Received"}') + return JSONResponse('{"data":"","success":0,"message":"No Data Received","type":"ir"}') learned = ''.join(format(x, '02x') for x in bytearray(data)) logger.info("IR Learn success") - return JSONResponse('{"data":"' + learned + '","success":1,"message":"IR Data Received"}') + return JSONResponse('{"data":"' + learned + '","success":1,"message":"IR Data Received","type":"ir"}') # Send IR/RF @@ -507,7 +511,7 @@ def sweep(request: Request, mac: str = "", host: str = "", type: str = "", comma logger.error("Device:" + host + " RF Frequency not found!") _rf_sweep_message = "RF Frequency not found!" dev.cancel_sweep_frequency() - return JSONResponse('{"data":"RF Frequency not found!","success":0}') + return JSONResponse('{"data":"RF Frequency not found!","success":0,"type":"rf"}') _rf_sweep_message = "Found RF Frequency - 1 of 2!" logger.info("Device:" + host + " Found RF Frequency - 1 of 2!") @@ -536,7 +540,7 @@ def sweep(request: Request, mac: str = "", host: str = "", type: str = "", comma else: logger.error("Device:" + host + " No Data Found!") _rf_sweep_message = "No Data Found" - return JSONResponse('{"data":"No Data Found"}') + return JSONResponse('{"data":"No Data Found","type":"rf","type":"rf"}') _rf_sweep_message = "Found RF Frequency - 2 of 2!" logger.info("Device:" + host + " Found RF Frequency - 2 of 2!") @@ -544,7 +548,7 @@ def sweep(request: Request, mac: str = "", host: str = "", type: str = "", comma _rf_sweep_message = "RF Scan Completed Successfully" logger.info("Device:" + host + " RF Scan Completed Successfully") time.sleep(1) - return JSONResponse('{"data":"' + learned + '"}') + return JSONResponse('{"data":"' + learned + '","type":"rf"}') # Get RF Learning state @@ -655,6 +659,28 @@ def get_device_status(request: Request, host: str = ""): # endregion API Methods +@app.post("/api/code") +def create_code(code: Code): + return db.insert_code(code.CodeType, code.CodeName, code.Code) + +@app.put("/api/code/{code_id}") +def update_code(code_id: int, code: Code): + return db.update_code(code_id, code.CodeType, code.CodeName, code.Code) + +@app.delete("/api/code/{code_id}") +def delete_code(code_id: int): + return db.delete_code(code_id) + +@app.get("/api/code/{code_id}") +def read_code(code_id: int): + return db.select_code(code_id) + +@app.get("/api/codes") +def read_all_codes(): + return db.select_all_codes(api_call=True) + + + # Start Application if __name__ == '__main__': logger.info("Broadlink Manager is up and running") diff --git a/broadlinkmanager/code.py b/broadlinkmanager/code.py new file mode 100644 index 0000000..054e913 --- /dev/null +++ b/broadlinkmanager/code.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +class Code(BaseModel): + CodeId: int = None + CodeType: str + CodeName: str + Code: str \ No newline at end of file diff --git a/broadlinkmanager/dist/js/codes.js b/broadlinkmanager/dist/js/codes.js new file mode 100644 index 0000000..54aa6bb --- /dev/null +++ b/broadlinkmanager/dist/js/codes.js @@ -0,0 +1,151 @@ +$(document).ready(function () { + var table = $('#codesTable').DataTable(); + + // Load data into the table + $.ajax({ + url: '/api/codes/', // Change this to your API endpoint + type: 'GET', + dataType: 'json', + success: function (data) { + data.forEach(function (item) { + table.row.add([ + item.CodeName, + item.CodeType, + item.Code, + '', + '' + item.CodeId + '' + ]).draw(false); + }); + }, + error: function (xhr, status, error) { + Swal.fire({ + toast: true, + position: 'top', + customClass: { + toast: 'swal2-toast-top' + }, + icon: 'error', + title: 'Error loading data: ' + xhr.responseText, + showConfirmButton: false, + timer: 3000 + }); + } + }); + + // Handle edit button + $('#codesTable').on('click', '.edit', function () { + var row = $(this).closest('tr'); + row.find('td:not(:last-child)').each(function (index) { + var cell = $(this); + if (index < 3) { // Skip the last cell (actions) + var content = cell.text(); + cell.html(''); + } + }); + row.find('.edit').hide(); + row.find('.save').show(); + }); + + // Handle save button + $('#codesTable').on('click', '.save', function () { + var row = $(this).closest('tr'); + var data = { + CodeId: row.find('td:eq(4) span').text(), + CodeName: row.find('td:eq(0) input').val(), + CodeType: row.find('td:eq(1) input').val(), + Code: row.find('td:eq(2) input').val() + }; + + $.ajax({ + url: '/api/code/' + data.CodeId, // Change this to your API endpoint + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(data), + success: function (response) { + row.find('td:not(:last-child)').each(function (index) { + var cell = $(this); + if (index < 3) { // Skip the last cell (actions) + var input = cell.find('input'); + cell.text(input.val()); + } + }); + row.find('.edit').show(); + row.find('.save').hide(); + Swal.fire({ + toast: true, + position: 'top', + customClass: { + toast: 'swal2-toast-top' + }, + icon: 'success', + title: 'Code updated successfully', + showConfirmButton: false, + timer: 3000 + }); + }, + error: function (xhr, status, error) { + Swal.fire({ + toast: true, + position: 'top', + customClass: { + toast: 'swal2-toast-top' + }, + icon: 'error', + title: 'Error updating code: ' + xhr.responseText, + showConfirmButton: false, + timer: 3000 + }); + } + }); + }); + + // Handle delete button + $('#codesTable').on('click', '.delete', function () { + var row = $(this).closest('tr'); + var codeId = row.find('td:eq(4) span').text(); + + Swal.fire({ + title: 'Are you sure?', + text: 'You won\'t be able to revert this!', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Yes, delete it!' + }).then((result) => { + if (result.isConfirmed) { + $.ajax({ + url: '/api/code/' + codeId, // Change this to your API endpoint + type: 'DELETE', + success: function (response) { + table.row(row).remove().draw(false); + Swal.fire({ + toast: true, + position: 'top', + customClass: { + toast: 'swal2-toast-top' + }, + icon: 'success', + title: 'Code deleted successfully', + showConfirmButton: false, + timer: 3000 + }); + }, + error: function (xhr, status, error) { + Swal.fire({ + toast: true, + position: 'top', + customClass: { + toast: 'swal2-toast-top' + }, + icon: 'error', + title: 'Error deleting code: ' + xhr.responseText, + showConfirmButton: false, + timer: 3000 + }); + } + }); + } + }); + }); +}); \ No newline at end of file diff --git a/broadlinkmanager/dist/js/home.js b/broadlinkmanager/dist/js/home.js index 1402ab7..ca70433 100644 --- a/broadlinkmanager/dist/js/home.js +++ b/broadlinkmanager/dist/js/home.js @@ -1,6 +1,85 @@ var con = 1; var RfStatus; -$(document).ready(function () { +$(document).ready(function(){ + + + $("#savecode").click(function() { + var button = $(this); + button.prop("disabled", true); + var codeType = $('#code_type').val().trim(); + var codeName = $('#codename').val().trim(); + var code = $('#data').val().trim(); + + if (!codeType || !codeName || !code) { + Swal.fire({ + toast: true, + position: 'bottom-end', + icon: 'error', + title: 'Code Type, Code Name, and Code cannot be empty', + showConfirmButton: false, + timer: 3000 + }); + return; + } + + + + var codeData = { + CodeType: codeType, + CodeName: codeName, + Code: code + }; + + $.ajax({ + url: '/api/code', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(codeData), + success: function (response) { + console.log(response.success); + if(response.success==1){ + Swal.fire({ + toast: true, + position: 'bottom-end', + icon: 'success', + title: response.message, + showConfirmButton: false, + timer: 3000 + }); + $('#data').val(''); + $('#codename').val(''); + $('#message').text(''); + $('#extend').hide(); + } + else{ + Swal.fire({ + toast: true, + position: 'bottom-end', + icon: 'error', + title: 'Error creating code: ' + response.message, + showConfirmButton: false, + timer: 3000 + }); + } + + }, + error: function (xhr, status, error) { + console.log(error); + } + + }); + // // Disable the button + // button.prop("disabled", true); + + // // Your click event logic here + // console.log("This will be displayed only once."); + + // // Re-enable the button after 2-3 seconds (e.g., 2500 milliseconds) + + // $('#extend').hide(); + // button.prop("disabled", false); + button.prop("disabled", false); + }); $("#rescan").click(function () { $("#scan").hide(); @@ -20,7 +99,7 @@ $(document).ready(function () { success: function (data) { showDevices(data); - localStorage.setItem('devices',JSON.stringify(data)); + localStorage.setItem('devices', JSON.stringify(data)); }, @@ -109,7 +188,7 @@ $(document).ready(function () { }); -AutoPing(); + AutoPing(); }); @@ -127,7 +206,7 @@ function getDevices(url) { } }); - GetDeviceStatus(); + GetDeviceStatus(); } function showDevices(data) { @@ -177,6 +256,8 @@ function learnIr(_type, _host, _mac) { $("#message").text(data.message); $("#message").css('color', 'green'); $('#data').val(hexToBase64(data.data)); + $('#extend').show(); + $('#code_type').val(data.type); } $("#scaning").hide(); $("#data-wrapper").show(); @@ -234,6 +315,8 @@ function learnrf(_type, _host, _mac) { else { $('#data').val(hexToBase64(data.data)); $('#message').text("RF Scan Completed Successfully"); + $('#code_type').val(data.type); + $('#extend').show(); } clearInterval(RfStatus); @@ -277,12 +360,12 @@ function GetDeviceStatus() { $('td[id^="_ip_"]').each(function () { ip = $(this).text(); status_id = '#_status_' + $(this).attr('id').match(/\d+/)[0]; - ping(ip,status_id); + ping(ip, status_id); }); } function ping(host, status_id) { - + $.ajax( { url: 'device/ping?host=' + host, @@ -309,7 +392,7 @@ function ping(host, status_id) { } function AutoPing() { - timer = setInterval(function() { + timer = setInterval(function () { GetDeviceStatus(); }, 60000); } @@ -346,7 +429,7 @@ function Table2Json() { dataType: 'json' }); - + } diff --git a/broadlinkmanager/sqliteconnector.py b/broadlinkmanager/sqliteconnector.py new file mode 100644 index 0000000..1f0c645 --- /dev/null +++ b/broadlinkmanager/sqliteconnector.py @@ -0,0 +1,142 @@ +from loguru import logger +import sqlite3 + +class SqliteConnector: + def __init__(self): + self.db_path = "data/codes.db" + self.conn = None + # Connect to the SQLite database + def open_connection(self): + if self.conn is None: + try: + self.conn = sqlite3.connect(self.db_path) + logger.info("Connection opened successfully.") + except sqlite3.Error as e: + logger.error(f"Error connecting to database: {e}") + else: + logger.debug("Connection is already open.") + + + def close_connection(self): + if self.conn is not None: + self.conn.close() + self.conn = None + logger.info("Connection closed.") + else: + logger.info("No connection to close.") + + + def create_tables(self): + try: + self.open_connection() + cursor = self.conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS Codes ( + CodeId INTEGER PRIMARY KEY AUTOINCREMENT, + CodeType TEXT NOT NULL, + CodeName TEXT NOT NULL, + Code TEXT NOT NULL) + ''') + + self.conn.commit() + self.close_connection() + logger.info("Tables created successfully") + except sqlite3.Error as e: + logger.error(str(e)) + + def execute_query(self, query, params=(),is_insert=False): + self.open_connection() + cursor = self.conn.cursor() + cursor.execute(query, params) + self.conn.commit() + if is_insert: + lastrowid = cursor.lastrowid + self.close_connection() + return lastrowid + self.close_connection() + + + # Inserts + + def insert_code(self, code_type, code_name, code): + try: + query = 'INSERT INTO Codes (CodeType, CodeName, Code) VALUES (?, ?, ?)' + self.execute_query(query, (code_type, code_name, code)) + return {"message": "Code inserted successfully.","success":1} + except Exception as e: + logger.error(f"Failed to update the code. {str(e)}") + return {"message": f"Failed to update the code. {str(e)}","success":0} + + # Updates + + def update_code(self, code_id, code_type=None, code_name=None, code=None): + try: + query = 'UPDATE Codes SET ' + params = [] + if code_type: + query += 'CodeType=?, ' + params.append(code_type) + if code_name: + query += 'CodeName=?, ' + params.append(code_name) + if code: + query += 'Code=?, ' + params.append(code) + query = query.rstrip(', ') + ' WHERE CodeId=?' + params.append(code_id) + self.execute_query(query, params) + return {"message": "Code updated successfully.","success":1} + except Exception as e: + logger.error(f"Failed to update the code. {str(e)}") + return {"message": f"Failed to update the code. {str(e)}","success":0} + + # Deletes + + def delete_code(self, code_id): + try: + query = 'DELETE FROM Codes WHERE CodeId = ?' + self.execute_query(query, (code_id,)) + return {"message": "Code deleted successfully.","success":1} + except Exception as e: + logger.error(f"Failed to update the code. {str(e)}") + return {"message": f"Failed to delete the code. {str(e)}","success":0} + # Selects + + def select_code(self,code_id, api_call=True): + try: + logger.warning(code_id) + self.open_connection() + cursor = self.conn.cursor() + cursor.execute(f"SELECT CodeId, CodeType, CodeName, Code FROM Codes where CodeId={code_id}") + if api_call: + rows = [dict((cursor.description[i][0], value) for i, value in enumerate(row)) for row in cursor.fetchall()] + cursor.close() + return rows + else: + rows = cursor.fetchall() + cursor.close() + return rows + except Exception as e: + logger.error(f"Failed to get the code. {str(e)}") + return [] + finally: + self.close_connection() + + def select_all_codes(self, api_call=True): + try: + self.open_connection() + cursor = self.conn.cursor() + cursor.execute('SELECT CodeId, CodeType, CodeName, Code FROM Codes') + if api_call: + rows = [dict((cursor.description[i][0], value) for i, value in enumerate(row)) for row in cursor.fetchall()] + cursor.close() + return rows + else: + rows = cursor.fetchall() + cursor.close() + return rows + except Exception as e: + logger.error(f"Failed to update the code. {str(e)}") + return [] + finally: + self.close_connection() \ No newline at end of file diff --git a/broadlinkmanager/templates/about.html b/broadlinkmanager/templates/about.html index 97b3d6a..ae3cb16 100644 --- a/broadlinkmanager/templates/about.html +++ b/broadlinkmanager/templates/about.html @@ -55,6 +55,12 @@

Devices

+ + + + + + +