├── README.md ├── custom_components └── ha_qqmail │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── local │ ├── template │ │ ├── info.html │ │ └── state.html │ └── tips.html │ ├── manifest.json │ ├── qqmail.py │ ├── services.yaml │ └── translations │ └── en.json └── hacs.json /README.md: -------------------------------------------------------------------------------- 1 | # ha_qqmail 2 | 在HA里使用的QQ邮箱通知 3 | 4 | # 使用方式 5 | 6 | ```yaml 7 | 8 | # 配置后生成【ha_qqmail.notify】服务 9 | ha_qqmail: 10 | qq: QQ号码 11 | code: QQ邮箱授权码 12 | 13 | ``` 14 | 15 | > 调用服务格式 16 | ```yaml 17 | title: 标题 18 | message: 消息内容 19 | data: 20 | url: https://github.com/shaonianzhentan/ha_qqmail 21 | image: https://vuejs.org/images/logo.png 22 | actions: 23 | - action: close_light 24 | title: 关灯 25 | - action: close_all 26 | title: 关掉全部灯 27 | ``` 28 | 29 | > 获取点击事件(参考链接:https://www.home-assistant.io/integrations/html5/) 30 | ```yaml 31 | - alias: 这是一个H5的通知提示 32 | trigger: 33 | platform: event 34 | event_type: html5_notification.clicked 35 | event_data: 36 | action: close_light 37 | ``` 38 | 39 | # 更新日志 40 | 41 | ### v1.4 42 | - 新增查询全部实体状态功能 43 | - 更换HA要删除的属性 44 | - 兼容新版HA服务配置 45 | - 支持集成添加 46 | - 修复合并代码,在最新版HA运行异常的问题 47 | - 新增MQTT提醒服务 48 | 49 | ### v1.3 50 | - 重构代码逻辑 51 | - 删除外网链接配置 52 | - 兼容摄像头实体ID和图片 53 | - 新增url参数,点击标题可以跳转指定链接 54 | 55 | ### v1.2.1 56 | - 加入图片发送 57 | 58 | ### v1.2 59 | - 重新邮件通知逻辑,兼容html5通知 60 | 61 | ### v1.1 62 | - 支持执行多个脚本或自动化 63 | - 模板显示实体状态 64 | - 修复代码错误 65 | - 兼容yaml列表格式 66 | 67 | ### v1.0 68 | - 发送通知到QQ邮箱 69 | - 发送通知到其它邮箱 70 | - 支持模板消息 71 | - 支持执行脚本或自动化 72 | 73 | 74 | ## 如果这个项目对你有帮助,请我喝杯咖啡奶茶吧😘 75 | |支付宝|微信| 76 | |---|---| 77 | 支付宝 | 微信支付 -------------------------------------------------------------------------------- /custom_components/ha_qqmail/__init__.py: -------------------------------------------------------------------------------- 1 | import os, uuid, logging 2 | 3 | from homeassistant.helpers.network import get_url 4 | from homeassistant.components.http import HomeAssistantView 5 | from aiohttp import web 6 | from .qqmail import QQMail 7 | 8 | from .const import DOMAIN, VERSION, URL, ROOT_PATH 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | def setup(hass, config): 13 | # 没有配置和已经运行则不操作 14 | if DOMAIN not in config or DOMAIN in hass.data: 15 | return True 16 | # 默认使用自定义的外部链接 17 | base_url = get_url(hass).strip('/') 18 | # 显示插件信息 19 | _LOGGER.info(''' 20 | ------------------------------------------------------------------- 21 | QQ邮箱通知插件【作者QQ:635147515】 22 | 23 | 版本:''' + VERSION + ''' 24 | 25 | API地址:''' + URL + ''' 26 | 27 | HA地址:''' + base_url + ''' 28 | 29 | 项目地址:https://github.com/shaonianzhentan/ha_qqmail 30 | -------------------------------------------------------------------''') 31 | # 读取配置 32 | cfg = config[DOMAIN] 33 | _qq = str(cfg.get('qq')) + '@qq.com' 34 | _code = cfg.get('code') 35 | # 定义QQ邮箱实例 36 | qm = QQMail(hass, _qq, _code, base_url + URL) 37 | hass.data[DOMAIN] = qm 38 | # 设置QQ邮箱通知服务 39 | if hass.services.has_service(DOMAIN, 'notify') == False: 40 | hass.services.async_register(DOMAIN, 'notify', qm.notify) 41 | # 注册静态目录 42 | hass.http.register_static_path(ROOT_PATH, hass.config.path('custom_components/' + DOMAIN + '/local'), False) 43 | # 注册事件网关 44 | hass.http.register_view(HassGateView) 45 | return True 46 | 47 | # 集成安装 48 | async def async_setup_entry(hass, entry): 49 | setup(hass, { DOMAIN: entry.data }) 50 | return True 51 | 52 | class HassGateView(HomeAssistantView): 53 | 54 | url = URL 55 | name = DOMAIN 56 | requires_auth = False 57 | 58 | async def get(self, request): 59 | # 这里进行重定向 60 | hass = request.app["hass"] 61 | if 'action' in request.query: 62 | action = request.query['action'] 63 | # 触发事件 64 | hass.bus.fire("html5_notification.clicked", {"action": action}) 65 | return web.HTTPFound(location= ROOT_PATH + '/tips.html?msg=触发事件成功&id=' + action) 66 | else: 67 | return self.json({'code': '401', 'msg': '参数不正确'}) 68 | -------------------------------------------------------------------------------- /custom_components/ha_qqmail/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Hello World integration.""" 2 | import logging 3 | 4 | import voluptuous as vol 5 | 6 | from homeassistant import config_entries 7 | 8 | from .const import DOMAIN # pylint:disable=unused-import 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | DATA_SCHEMA = vol.Schema({ 13 | vol.Required("qq", default = ""): str, 14 | vol.Required("code", default = ""): str 15 | }) 16 | 17 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 18 | 19 | VERSION = 1 20 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 21 | 22 | async def async_step_user(self, user_input=None): 23 | errors = {} 24 | if DOMAIN in self.hass.data: 25 | return self.async_abort(reason="single_instance_allowed") 26 | 27 | # 如果输入内容不为空,则进行验证 28 | if user_input is not None: 29 | return self.async_create_entry(title="", data=user_input) 30 | 31 | # 显示表单 32 | return self.async_show_form( 33 | step_id="user", data_schema=DATA_SCHEMA, errors=errors 34 | ) -------------------------------------------------------------------------------- /custom_components/ha_qqmail/const.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | DOMAIN = 'ha_qqmail' 4 | VERSION = '1.4' 5 | URL = '/' + DOMAIN + '-api-' + str(uuid.uuid4()) 6 | ROOT_PATH = '/' + DOMAIN +'-local/' + VERSION -------------------------------------------------------------------------------- /custom_components/ha_qqmail/local/template/info.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {CONTENT} 4 |
5 |
6 | {BUTTON} 7 |
8 |
-------------------------------------------------------------------------------- /custom_components/ha_qqmail/local/template/state.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {CONTENT} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
设备类型实体名称状态
automation新建自动化on
binary_sensorUpdateron
media_playerha_cloud_musicidle
media_playerVlcidle
personxiaobaiunknown
script你好=我好off
sunSunabove_horizon
weather我的家rainy
zone我的家zoning
48 |
49 |
50 | {BUTTON} 51 |
52 |
-------------------------------------------------------------------------------- /custom_components/ha_qqmail/local/tips.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HomeAssistant通知提醒 6 | 7 | 9 | 31 | 32 | 33 | 34 |
35 | HomeAssistant通知提醒 36 |
37 |
38 | 39 |

40 | 41 |

42 | 43 |
44 | 45 |
46 | 47 | 通知来自 ha_qqmail 48 | 49 | 54 | 55 | -------------------------------------------------------------------------------- /custom_components/ha_qqmail/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ha_qqmail", 3 | "name": "QQ\u90AE\u7BB1\u901A\u77E5", 4 | "version": "1.0", 5 | "config_flow": true, 6 | "documentation": "https://github.com/shaonianzhentan/ha_qqmail", 7 | "requirements": [], 8 | "dependencies": [], 9 | "codeowners": [] 10 | } -------------------------------------------------------------------------------- /custom_components/ha_qqmail/qqmail.py: -------------------------------------------------------------------------------- 1 | import requests, base64, json 2 | import urllib.parse 3 | from homeassistant.helpers import template 4 | import logging 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | # ----------邮件相关---------- # 8 | from email import encoders 9 | from email.header import Header 10 | from email.mime.text import MIMEText 11 | from email.utils import parseaddr, formataddr 12 | import smtplib 13 | 14 | def _format_addr(s): 15 | name, addr = parseaddr(s) 16 | return formataddr((Header(name, 'utf-8').encode(), addr)) 17 | # ----------邮件相关---------- # 18 | 19 | class QQMail: 20 | 21 | # 初始化 22 | def __init__(self, hass, from_addr, password, api_url): 23 | self.hass = hass 24 | self.from_addr = from_addr 25 | self.password = password 26 | self.api_url = api_url 27 | 28 | # 发送邮件 29 | def sendMail(self, to_addr, _title, _message): 30 | try: 31 | from_addr = self.from_addr 32 | smtp_server = 'smtp.qq.com' 33 | msg = MIMEText(_message, 'html', 'utf-8') 34 | msg['From'] = _format_addr('HomeAssistant <%s>' % from_addr) 35 | #msg['To'] = _format_addr('智能家居 <%s>' % to_addr) 36 | msg['To'] = ','.join(to_addr) 37 | msg['Subject'] = Header(_title, 'utf-8').encode() 38 | server = smtplib.SMTP(smtp_server, 25) 39 | server.set_debuglevel(1) 40 | server.login(from_addr, self.password) 41 | server.sendmail(from_addr, to_addr, msg.as_string()) 42 | server.quit() 43 | _LOGGER.debug('【' + _title + '】邮件通知发送成功') 44 | except Exception as e: 45 | _LOGGER.debug('【' + _title + '】邮件通知发送失败') 46 | _LOGGER.debug(e) 47 | 48 | ######################## 功能 ######################## 49 | # 读取文件内容 50 | def getContent(self, _path): 51 | fp = open(_path, 'r', encoding='UTF-8') 52 | content = fp.read() 53 | fp.close() 54 | return content 55 | 56 | # 通知服务 57 | def notify(self, call): 58 | data = call.data 59 | # 读取服务参数 60 | _type = data.get('type', '') 61 | _email = data.get('email', self.from_addr) 62 | if not isinstance(_email,list): 63 | _email = [_email] 64 | _title = data.get('title', '') 65 | _message = self.template(data.get('message', '')) 66 | # 发送信息 67 | if self.hass.services.has_service('mqtt', 'publish'): 68 | self.hass.async_create_task(self.hass.services.async_call('mqtt', 'publish', { 69 | 'topic': 'ha-mqtt/notify', 70 | 'payload': json.dumps({ 'title': _title, 'message': _message }) 71 | })) 72 | # 在windows提示,阻止邮件 73 | if _type == 'windows': 74 | return 75 | 76 | _data = data.get('data', []) 77 | # 生成操作按钮 78 | _list = [] 79 | _action = '' 80 | if 'actions' in _data: 81 | for item in _data['actions']: 82 | _list.append({ 83 | 'name': item['title'], 84 | 'url': self.api_url + '?action=' + item['action'] 85 | }) 86 | if len(_list) > 0: 87 | _action = self.template(''' 88 | {% set actions = ''' + json.dumps(_list) + ''' %} 89 | {% for action in actions -%} 90 | {{ action.name }} 91 | {%- endfor %} ''') 92 | 93 | # 生成图片 94 | if 'image' in _data: 95 | _image = _data['image'] 96 | _LOGGER.debug('图片地址:' + _image) 97 | # 判断是否图片地址 98 | if 'https://' in _image or 'http://' in _image: 99 | _image = self.url_to_base64(_image) 100 | else: 101 | # 如果当前图片传入的是摄像头实体ID,则使用实体图片 102 | entity_id = _image 103 | state = self.hass.states.get(entity_id) 104 | if state is not None: 105 | entity_picture = self.hass.config.api.base_url + state.attributes['entity_picture'] 106 | _LOGGER.debug('摄像头图片地址:' + entity_picture) 107 | # 进行图片格式转换 108 | _image = self.url_to_base64(entity_picture) 109 | else: 110 | _image = '' 111 | # 生成图片标签 112 | if _image != '': 113 | _message += '' 114 | 115 | # 传入URL链接 116 | if 'url' in data: 117 | _LOGGER.debug('URL链接地址:' + data['url']) 118 | _title = '' + _title + '' 119 | 120 | # 如果类型是状态,则查询全部状态 121 | if _type == 'state': 122 | _message += self.template(''' 123 | 124 | 125 | 126 | 127 | 128 | 129 | {% for state in states -%} 130 | 131 | 132 | 133 | 134 | 135 | {%- endfor %} 136 |
设备类型实体名称状态
{{ state.domain }}{{ state.name }}{{state.state}}
137 | ''') 138 | # 读取消息模板 139 | template_info = self.getContent(self.hass.config.path("custom_components/ha_qqmail/local/template/info.html")) 140 | 141 | # 替换标题和信息,重新生成消息 142 | _message = template_info.replace('{CONTENT}', _message).replace('{BUTTON}', _action) 143 | # 发送邮件 144 | _LOGGER.debug(_message) 145 | self.sendMail(_email, _title, _message) 146 | 147 | # 模板解析 148 | def template(self, _message): 149 | _LOGGER.debug('【模板解析前】:' + _message) 150 | # 解析模板 151 | tpl = template.Template(_message, self.hass) 152 | _message = tpl.async_render(None) 153 | _LOGGER.debug('【模板解析后】:' + _message) 154 | return _message 155 | 156 | # 图片转base64 157 | def url_to_base64(self, img_url): 158 | r = requests.get(img_url, headers={}, stream=True) 159 | if r.status_code == 200: 160 | base64_data = base64.b64encode(r.content) 161 | s = base64_data.decode() 162 | return 'data:image/jpeg;base64,' + s 163 | return None 164 | -------------------------------------------------------------------------------- /custom_components/ha_qqmail/services.yaml: -------------------------------------------------------------------------------- 1 | notify: 2 | description: QQ邮件通知提醒 3 | fields: 4 | type: 5 | name: 邮件类型 6 | description: '【非必填】state: 显示全部实体状态' 7 | example: state 8 | selector: 9 | text: 10 | email: 11 | name: 邮件地址 12 | description: 【非必填】如果不填,发送给自己 13 | example: '635147515@qq.com' 14 | selector: 15 | text: 16 | title: 17 | name: 邮件标题 18 | description: 【必填】邮件标题 19 | example: 邮件标题 20 | selector: 21 | text: 22 | message: 23 | name: 邮件内容 24 | description: 【非必填】要提醒的文字(可以为HTML格式和使用系统模板) 25 | example: '当前时间: {{now()}} ' 26 | selector: 27 | text: 28 | data: 29 | name: 额外数据 30 | description: 通知参考链接:https://www.home-assistant.io/integrations/html5/ 31 | example: '' 32 | selector: 33 | text: -------------------------------------------------------------------------------- /custom_components/ha_qqmail/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "description": "项目: https://github.com/shaonianzhentan/ha_qqmail", 6 | "data": { 7 | "qq": "QQ号码", 8 | "code": "QQ邮箱授权码" 9 | } 10 | } 11 | }, 12 | "error": {}, 13 | "abort": { 14 | "single_instance_allowed": "仅允许单个配置." 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QQ邮箱通知", 3 | "country": "CN", 4 | "render_readme": true, 5 | "domains": [] 6 | } --------------------------------------------------------------------------------