├── 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 | automation |
12 | 新建自动化 |
13 | on |
14 |
15 | binary_sensor |
16 | Updater |
17 | on |
18 |
19 | media_player |
20 | ha_cloud_music |
21 | idle |
22 |
23 | media_player |
24 | Vlc |
25 | idle |
26 |
27 | person |
28 | xiaobai |
29 | unknown |
30 |
31 | script |
32 | 你好=我好 |
33 | off |
34 |
35 | sun |
36 | Sun |
37 | above_horizon |
38 |
39 | weather |
40 | 我的家 |
41 | rainy |
42 |
43 | zone |
44 | 我的家 |
45 | zoning |
46 |
47 |
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 | {{ state.domain }} |
132 | {{ state.name }} |
133 | {{state.state}} |
134 |
135 | {%- endfor %}
136 |
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 | }
--------------------------------------------------------------------------------