├── .github └── workflows │ └── hassfest.yaml ├── .gitignore ├── README.md ├── custom_components └── mysql_query │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ └── services.yaml └── hacs.json /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | /custom_components/mysql_query/__pycache__ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MySQL Query for Home Assistant 2 | 3 | [![HACS Custom][hacs_shield]][hacs] 4 | [![GitHub Latest Release][releases_shield]][latest_release] 5 | [![GitHub All Releases][downloads_total_shield]][releases] 6 | [![Community Forum][community_forum_shield]][community_forum] 7 | 8 | A Home Assistant custom component that provides a *ResponseData service* to execute MySQL database queries. The results are available as an iterable data structure. 9 | 10 | ## Key Features 11 | 12 | - Support for all SQL query types (since V1.4.0) 13 | - SELECT and WITH statements for data retrieval 14 | - INSERT, UPDATE, and DELETE statements for data manipulation 15 | - Multiple database support 16 | - Full integration with Home Assistant automations 17 | 18 | ⚠️ **WARNING**: Exercise caution with destructive statements like UPDATE, DELETE, or DROP. The developer takes no responsibility for any data loss. 19 | 20 | ## Requirements 21 | 22 | - Home Assistant version 2023.7 or newer (due to Responding services functionality) 23 | 24 | ## Installation 25 | 26 | ### Option 1: Using HACS (recommended) 27 | 28 | 1. Open HACS in your Home Assistant installation 29 | 2. Add this repository as a custom repository: `https://github.com/IAsDoubleYou/homeassistant-mysql_query` 30 | 3. Search for "MySQL Query" in HACS and install 31 | 32 | ### Option 2: Manual Installation 33 | 34 | 1. Navigate to your Home Assistant configuration directory (where `configuration.yaml` is located) 35 | 2. Create a `custom_components` directory if it doesn't exist 36 | 3. Create a new directory called `mysql_query` in `custom_components` 37 | 4. Download all files from the `custom_components/mysql_query/` directory of this repository 38 | 5. Place the downloaded files in the new directory 39 | 6. Restart Home Assistant 40 | 7. Add the configuration (see below) 41 | 8. Restart Home Assistant again 42 | 43 | ## Configuration 44 | 45 | Add the following configuration to your `configuration.yaml`: 46 | 47 | ```yaml 48 | mysql_query: 49 | mysql_host: # Required 50 | mysql_username: # Required 51 | mysql_password: # Required 52 | mysql_db: # Required 53 | mysql_port: # Optional, defaults to 3306 54 | mysql_autocommit: # Optional, defaults to true 55 | mysql_charset: # Optional 56 | mysql_collation: # Optional 57 | ``` 58 | 59 | ### Character Set and Collation 60 | 61 | Optionally, you can configure a specific character set and collation: 62 | 63 | ```yaml 64 | mysql_charset: utf8mb4 65 | mysql_collation: utf8mb4_unicode_ci 66 | ``` 67 | 68 | ### Autocommit 69 | 70 | - `mysql_autocommit: true` (default): Each statement is committed immediately 71 | - `mysql_autocommit: false`: Transactions must be explicitly committed or rolled back using COMMIT or ROLLBACK statements 72 | 73 | ## Usage 74 | 75 | ### Basic Query Service 76 | 77 | ```yaml 78 | service: mysql_query.query 79 | data: 80 | query: "SELECT * FROM contacts WHERE phonenumber='1234567890'" 81 | ``` 82 | 83 | ### Query with Alternative Database 84 | 85 | ```yaml 86 | service: mysql_query.query 87 | data: 88 | query: "SELECT * FROM contacts" 89 | db4query: alternative_db 90 | ``` 91 | 92 | ### Response Format 93 | 94 | The service returns results in YAML format: 95 | 96 | ```yaml 97 | result: 98 | - phonenumber: "0111111111" 99 | announcement: "Announcement for phonenumber 0111111111" 100 | language: "en" 101 | - phonenumber: "0222222222" 102 | announcement: "Announcement for phonenumber 0222222222" 103 | language: "en" 104 | ``` 105 | 106 | ### Automation Example 107 | 108 | ```yaml 109 | alias: mysql_query test 110 | description: "Example of MySQL Query in automation" 111 | trigger: [] 112 | condition: [] 113 | action: 114 | - variables: 115 | response: null 116 | - service: mysql_query.query 117 | data: 118 | query: "SELECT * FROM contacts" 119 | response_variable: response 120 | - service: notify.your_gmail_com 121 | data: 122 | message: |- 123 | Response from MySQL Query Service: 124 | {% for item in response.result %} 125 | {{ item.phonenumber }} 126 | {{ item.announcement }} 127 | {{ item.language }} 128 | {% endfor %} 129 | title: "Test of MySQL Query Service" 130 | target: youraccount@gmail.com 131 | mode: single 132 | ``` 133 | 134 | ## Related Projects 135 | 136 | Also check out: 137 | - [HA MySQL](https://github.com/IAsDoubleYou/ha_mysql) component for a MySQL *sensor* component. 138 | - [coinbase_crypto_monitor](https://github.com/IAsDoubleYou/coinbase_crypto_monitor) component for a coinbase monitor sensor component. 139 | 140 | [hacs_shield]: https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge 141 | [hacs]: https://github.com/hacs/integration 142 | [latest_release]: https://github.com/IAsDoubleYou/homeassistant-mysql_query/releases/latest 143 | [releases_shield]: https://img.shields.io/github/release/IAsDoubleYou/homeassistant-mysql_query.svg?style=for-the-badge 144 | [releases]: https://github.com/IAsDoubleYou/homeassistant-mysql_query/releases/ 145 | [downloads_total_shield]: https://img.shields.io/github/downloads/IAsDoubleYou/homeassistant-mysql_query/total?style=for-the-badge 146 | [community_forum_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Forum&style=for-the-badge&color=41bdf5&logo=HomeAssistant&logoColor=white 147 | [community_forum]: https://community.home-assistant.io/t/mysql-query/734346 148 | -------------------------------------------------------------------------------- /custom_components/mysql_query/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for mysql_query service.""" 2 | 3 | from __future__ import annotations 4 | 5 | import mysql.connector 6 | import logging 7 | import voluptuous as vol 8 | import homeassistant.helpers.config_validation as cv 9 | from functools import partial 10 | from homeassistant.core import HomeAssistant, SupportsResponse 11 | from homeassistant.helpers.typing import ConfigType 12 | from homeassistant.exceptions import HomeAssistantError 13 | from typing import Final 14 | 15 | DOMAIN = "mysql_query" 16 | SERVICE = "query" 17 | 18 | ATTR_QUERY = "query" 19 | ATTR_DB4QUERY = "db4query" 20 | CONF_MYSQL_HOST = "mysql_host" 21 | CONF_MYSQL_USERNAME = "mysql_username" 22 | CONF_MYSQL_PASSWORD = "mysql_password" 23 | CONF_MYSQL_DB = "mysql_db" 24 | CONF_MYSQL_PORT = "mysql_port" 25 | CONF_MYSQL_TIMEOUT = "mysql_timeout" 26 | CONF_MYSQL_CHARSET = "mysql_charset" 27 | CONF_MYSQL_COLLATION = "mysql_collation" 28 | CONF_AUTOCOMMIT = "mysql_autocommit" 29 | QUERY = "query" 30 | DB4QUERY = "db4query" 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | # Defaults 35 | DEFAULT_MYSQL_TIMEOUT = 10 36 | DEFAULT_MYSQL_PORT = 3306 37 | DEFAULT_MYSQL_AUTOCOMMIT = True 38 | 39 | CONFIG_SCHEMA = vol.Schema( 40 | { 41 | DOMAIN: vol.Schema( 42 | { 43 | vol.Required(CONF_MYSQL_HOST): cv.string, 44 | vol.Optional(CONF_MYSQL_PORT, default=DEFAULT_MYSQL_PORT): vol.Coerce( 45 | int 46 | ), 47 | vol.Required(CONF_MYSQL_USERNAME): cv.string, 48 | vol.Required(CONF_MYSQL_PASSWORD): cv.string, 49 | vol.Required(CONF_MYSQL_DB): cv.string, 50 | vol.Optional(CONF_MYSQL_TIMEOUT, default=DEFAULT_MYSQL_TIMEOUT): vol.Coerce(int), 51 | vol.Optional(CONF_MYSQL_CHARSET): cv.string, 52 | vol.Optional(CONF_MYSQL_COLLATION): cv.string, 53 | vol.Optional(CONF_AUTOCOMMIT, default=DEFAULT_MYSQL_AUTOCOMMIT): cv.boolean, 54 | } 55 | ), 56 | }, 57 | extra=vol.ALLOW_EXTRA, 58 | ) 59 | 60 | SERVICE_QUERY: Final = "query" 61 | SERVICE_DB4QUERY: Final = "db4query" 62 | 63 | SERVICE_QUERY_SCHEMA: Final = vol.All( 64 | cv.has_at_least_one_key(QUERY), cv.has_at_most_one_key(QUERY) 65 | ) 66 | 67 | 68 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 69 | conf = config[DOMAIN] 70 | mysql_host = conf.get(CONF_MYSQL_HOST) 71 | mysql_port = conf.get(CONF_MYSQL_PORT, DEFAULT_MYSQL_PORT) 72 | mysql_username = conf.get(CONF_MYSQL_USERNAME) 73 | mysql_password = conf.get(CONF_MYSQL_PASSWORD) 74 | mysql_db = conf.get(CONF_MYSQL_DB) 75 | mysql_timeout = conf.get(CONF_MYSQL_TIMEOUT, DEFAULT_MYSQL_TIMEOUT) 76 | mysql_collation = conf.get(CONF_MYSQL_COLLATION, None) 77 | mysql_charset = conf.get(CONF_MYSQL_CHARSET, None) 78 | mysql_autocommit = conf.get(CONF_AUTOCOMMIT, DEFAULT_MYSQL_AUTOCOMMIT) 79 | 80 | # Standard required connection arguments 81 | connect_kwargs = { 82 | "host": mysql_host, 83 | "user": mysql_username, 84 | "password": mysql_password, 85 | "database": mysql_db, 86 | "port": str(mysql_port), 87 | "autocommit": mysql_autocommit, 88 | } 89 | 90 | # Additional, optional connection arguments 91 | if mysql_charset is not None: 92 | connect_kwargs["charset"] = mysql_charset 93 | if mysql_collation is not None: 94 | connect_kwargs["collation"] = mysql_collation 95 | 96 | try: 97 | _LOGGER.info(f"Establishing connection with database { 98 | mysql_db} at {mysql_host}:{mysql_port}") 99 | _cnx = await hass.async_add_executor_job(partial(mysql.connector.connect, **connect_kwargs)) 100 | 101 | except Exception as e: 102 | # Log the rror with the full stack trace 103 | _LOGGER.error("Could not connect to mysql server: %s", 104 | str(e), exc_info=True) 105 | _cnx = None 106 | raise HomeAssistantError( 107 | f"Could not connect to mysql server: {str(e)}") 108 | 109 | _LOGGER.info(f"Connection established with database { 110 | mysql_db} at {mysql_host}:{mysql_port}") 111 | hass.data["mysql_connection"] = _cnx 112 | 113 | def replace_blob_with_description(value): 114 | """ 115 | Check if the value is a large object and if so return a description instead of the value. 116 | """ 117 | if isinstance(value, (bytes, bytearray)): # Detect BLOBs 118 | return "BLOB" 119 | elif isinstance(value, memoryview): # Other large objects 120 | return "LARGE OBJECT" 121 | else: 122 | return value # Don't adjust other, normal values 123 | 124 | def handle_query(call): 125 | """Handle the service call.""" 126 | _query = call.data.get(ATTR_QUERY) 127 | _LOGGER.debug(f"received query: {_query}") 128 | 129 | _result = [] 130 | 131 | if _query != None: 132 | _db4query = call.data.get(ATTR_DB4QUERY, None) 133 | if ( 134 | (_db4query is not None) 135 | and (_db4query != "") 136 | and (_db4query.lower() != mysql_db.lower()) 137 | ): 138 | _LOGGER.debug(f"Provided database for this query: {_db4query}") 139 | 140 | try: 141 | # Override the default database with the one provided for this query 142 | connect_kwargs["database"] = _db4query 143 | 144 | _LOGGER.debug(f"Establishing connection with database { 145 | _db4query} at {mysql_host}:{mysql_port}") 146 | _cnx4qry = mysql.connector.connect(**connect_kwargs) 147 | 148 | except Exception as e: 149 | # Log the rror with the full stack trace 150 | _LOGGER.error( 151 | "Could not connect to mysql server: %s", str(e), exc_info=True) 152 | _cnx4qry = None 153 | raise HomeAssistantError( 154 | f"Could not connect to mysql server: {str(e)}") 155 | else: 156 | _cnx4qry = _cnx 157 | 158 | if _cnx4qry is not None: 159 | try: 160 | _cnx4qry.ping(reconnect=True) 161 | _cursor = _cnx4qry.cursor(buffered=True) 162 | _LOGGER.debug(f"Executing query: {_query}") 163 | _cursor.execute(_query) 164 | 165 | if _cursor.with_rows: 166 | _cols = _cursor.description 167 | _LOGGER.debug(f"Fetching all records") 168 | _rows = _cursor.fetchall() 169 | else: 170 | _rows = None 171 | 172 | except Exception as e: 173 | raise HomeAssistantError(f"{str(e)}") 174 | 175 | if _rows is not None: 176 | _i = 0 177 | _LOGGER.debug(f"Found {len(_rows)} rows") 178 | for _r, _row in enumerate(_rows): 179 | _i+1 180 | _LOGGER.debug(f"Fetching values") 181 | _values = {} 182 | for _c, _col in enumerate(_cols): 183 | _values[_col[0]] = replace_blob_with_description( 184 | _row[_c]) 185 | _result.append(_values) 186 | _LOGGER.debug(f"{_i}: _values: {_values}") 187 | else: 188 | _LOGGER.error("No query provided") 189 | raise HomeAssistantError("No query provided") 190 | 191 | _LOGGER.debug(f"Returning result: {_result}") 192 | return {"result": _result} 193 | 194 | hass.services.async_register( 195 | DOMAIN, 196 | SERVICE, 197 | handle_query, 198 | schema=SERVICE_QUERY_SCHEMA, 199 | supports_response=SupportsResponse.ONLY, 200 | ) 201 | 202 | _LOGGER.info("Service mysql_query is now set up") 203 | 204 | return True 205 | -------------------------------------------------------------------------------- /custom_components/mysql_query/const.py: -------------------------------------------------------------------------------- 1 | # const.py 2 | -------------------------------------------------------------------------------- /custom_components/mysql_query/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mysql_query", 3 | "name": "MySQL Query Service", 4 | "codeowners": [ 5 | "@IAsDoubleYou" 6 | ], 7 | "documentation": "https://github.com/IAsDoubleYou/homeassistant-mysql_query", 8 | "integration_type": "hub", 9 | "iot_class": "local_push", 10 | "issue_tracker": "https://github.com/IAsDoubleYou/homeassistant-mysql_query/issues", 11 | "requirements": [ 12 | "mysql-connector-python" 13 | ], 14 | "version": "1.4.1" 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/mysql_query/services.yaml: -------------------------------------------------------------------------------- 1 | query: 2 | name: MySQL Query 3 | description: Sends a SQL Query to a MySQL Database 4 | fields: 5 | query: 6 | name: Query 7 | description: The SQL query or command to execute 8 | example: 'SELECT column1, column2 FROM database WHERE column3=value' 9 | required: true 10 | selector: 11 | text: 12 | type: text 13 | db4query: 14 | name: Query Database 15 | description: An alternate database to query. 16 | example: 'database_name' 17 | required: false 18 | selector: 19 | text: 20 | type: text -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MySQL Query", 3 | "render_readme": true 4 | } 5 | --------------------------------------------------------------------------------