74 |
75 |
81 |
86 |
90 | ${
91 | entities.map(entity => {
92 | return html`
93 | ${entity}
94 | `;
95 | })
96 | }
97 |
98 |
99 |
100 |
101 |
106 |
110 | ${
111 | remotes.map(remote => {
112 | return html`
113 | ${remote}
114 | `;
115 | })
116 | }
117 |
118 |
119 |
120 |
125 |
129 | ${
130 | themes.map(theme => {
131 | return html`
132 | ${theme}
133 | `;
134 | })
135 | }
136 |
137 |
138 |
Roku TV?
144 |
145 |
Use the YAML editor if you need to specify custom services
146 |
147 | `;
148 | }
149 |
150 | renderStyle() {
151 | return html`
152 |
164 | `;
165 | }
166 |
167 | _valueChanged(ev) {
168 | if (!this._config || !this.hass) {
169 | return;
170 | }
171 | const target = ev.target;
172 | if (this[`_${target.configValue}`] === target.value) {
173 | return;
174 | }
175 | if (target.configValue) {
176 | if (target.value === "") {
177 | delete this._config[target.configValue];
178 | } else {
179 | this._config = {
180 | ...this._config,
181 | [target.configValue]:
182 | target.checked !== undefined ? target.checked : target.value
183 | };
184 | }
185 | }
186 | fireEvent(this, "config-changed", { config: this._config });
187 | }
188 | }
189 |
190 | customElements.define("tv-card-editor", TVCardEditor);
191 |
--------------------------------------------------------------------------------
/tv-card.js:
--------------------------------------------------------------------------------
1 | const LitElement = Object.getPrototypeOf(
2 | customElements.get("ha-panel-lovelace")
3 | );
4 | const html = LitElement.prototype.html;
5 |
6 | const keys = {
7 | "power": {"key": "KEY_POWER", "icon": "mdi:power"},
8 | "volume_up": {"key": "KEY_VOLUP", "icon": "mdi:volume-plus"},
9 | "volume_down": {"key": "KEY_VOLDOWN", "icon": "mdi:volume-minus"},
10 | "volume_mute": {"key": "KEY_MUTE", "icon": "mdi:volume-mute"},
11 | "return": {"key": "KEY_RETURN", "icon": "mdi:arrow-left"},
12 | "source": {"key": "KEY_SOURCE", "icon": "mdi:video-input-hdmi"},
13 | "info": {"key": "KEY_INFO", "icon": "mdi:television-guide"},
14 | "home": {"key": "KEY_HOME", "icon": "mdi:home"},
15 | "channel_up": {"key": "KEY_CHUP", "icon": "mdi:arrow-up"},
16 | "channel_down": {"key": "KEY_CHDOWN", "icon": "mdi:arrow-down"},
17 | "up": {"key": "KEY_UP", "icon": "mdi:chevron-up"},
18 | "left": {"key": "KEY_LEFT", "icon": "mdi:chevron-left"},
19 | "enter": {"key": "KEY_ENTER", "icon": "mdi:checkbox-blank-circle"},
20 | "right": {"key": "KEY_RIGHT", "icon": "mdi:chevron-right"},
21 | "down": {"key": "KEY_DOWN", "icon": "mdi:chevron-down"},
22 | "rewind": {"key": "KEY_REWIND", "icon": "mdi:rewind"},
23 | "play": {"key": "KEY_PLAY", "icon": "mdi:play"},
24 | "pause": {"key": "KEY_PAUSE", "icon": "mdi:pause"},
25 | "fast_forward": {"key": "KEY_FF", "icon": "mdi:fast-forward"},
26 | };
27 |
28 | const sources = {
29 | "netflix": {"source": "Netflix", "icon": "mdi:netflix"},
30 | "spotify": {"source": "Spotify", "icon": "mdi:spotify"},
31 | "youtube": {"source": "YouTube", "icon": "mdi:youtube"},
32 | };
33 |
34 | var fireEvent = function(node, type, detail, options) {
35 | options = options || {};
36 | detail = detail === null || detail === undefined ? {} : detail;
37 | var event = new Event(type, {
38 | bubbles: false,
39 | });
40 | event.detail = detail;
41 | node.dispatchEvent(event);
42 | return event;
43 | };
44 |
45 | class TVCardServices extends LitElement {
46 | constructor() {
47 | super();
48 |
49 | this.custom_keys = {};
50 | this.custom_sources = {};
51 | this.custom_icons = {};
52 |
53 | this.holdtimer = null;
54 | this.holdaction = null;
55 | this.holdinterval = null;
56 | this.timer = null;
57 | }
58 |
59 | static get properties() {
60 | return {
61 | _hass: {},
62 | _config: {},
63 | _apps: {},
64 | trigger: {},
65 | };
66 | }
67 |
68 | static getStubConfig() {
69 | return {};
70 | }
71 |
72 | getCardSize() {
73 | return 7;
74 | }
75 |
76 | setConfig(config) {
77 | if (!config.entity) {
78 | console.log("Invalid configuration");
79 | return;
80 | }
81 |
82 | this._config = { theme: "default", ...config };
83 | this.custom_keys = config.custom_keys || {};
84 | this.custom_sources = config.custom_sources || {};
85 | this.custom_icons = config.custom_icons || {};
86 |
87 | this.loadCardHelpers();
88 | this.renderVolumeSlider();
89 | }
90 |
91 | isButtonEnabled(row, button) {
92 | if (!(this._config[row] instanceof Array)) return false;
93 |
94 | return this._config[row].includes(button);
95 | }
96 |
97 | set hass(hass) {
98 | this._hass = hass;
99 | if (this.volume_slider) this.volume_slider.hass = hass;
100 | if (this._hassResolve) this._hassResolve();
101 | }
102 |
103 | get hass() {
104 | return this._hass;
105 | }
106 |
107 | async loadCardHelpers() {
108 | this._helpers = await (window).loadCardHelpers();
109 | if (this._helpersResolve) this._helpersResolve();
110 | }
111 |
112 | async renderVolumeSlider() {
113 | if (this._helpers === undefined)
114 | await new Promise((resolve) => (this._helpersResolve = resolve));
115 | if (this._hass === undefined)
116 | await new Promise((resolve) => (this._hassResolve = resolve));
117 | this._helpersResolve = undefined;
118 | this._hassResolve = undefined;
119 |
120 | let slider_config = {
121 | "type": "custom:my-slider",
122 | "entity": this._config.entity,
123 | "height": "50px",
124 | "mainSliderColor": "white",
125 | "secondarySliderColor": "rgb(60, 60, 60)",
126 | "mainSliderColorOff": "rgb(60, 60, 60)",
127 | "secondarySliderColorOff": "rgb(60, 60, 60)",
128 | "thumbWidth": "0px",
129 | "thumbHorizontalPadding": "0px",
130 | "radius": "25px",
131 | };
132 |
133 | if (this._config.slider_config instanceof Object) {
134 | slider_config = {...slider_config, ...this._config.slider_config };
135 | }
136 |
137 | this.volume_slider = await this._helpers.createCardElement(slider_config);
138 | this.volume_slider.style = "flex: 0.9;";
139 | this.volume_slider.ontouchstart = (e) => {
140 | e.stopImmediatePropagation();
141 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
142 | };
143 | this.volume_slider.addEventListener("input", (e) => {
144 | if (this._config.enable_slider_feedback === undefined || this._config.enable_slider_feedback) fireEvent(window, "haptic", "light");
145 | }, true);
146 |
147 | this.volume_slider.hass = this._hass;
148 | this.triggerRender();
149 | }
150 |
151 | sendKey(key) {
152 | let entity_id = this._config.entity;
153 |
154 | this._hass.callService("media_player", "play_media", {
155 | media_content_id: key,
156 | media_content_type: "send_key",
157 | }, { entity_id: entity_id });
158 | }
159 |
160 | changeSource(source) {
161 | let entity_id = this._config.entity;
162 |
163 | this._hass.callService("media_player", "select_source", {
164 | source: source,
165 | entity_id: entity_id,
166 | });
167 | }
168 |
169 | onClick(event) {
170 | event.stopImmediatePropagation();
171 | let click_action = () => {
172 | this.sendKey("KEY_ENTER");
173 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
174 | };
175 | if (this._config.enable_double_click) {
176 | this.timer = setTimeout(click_action, 200);
177 | } else {
178 | click_action();
179 | }
180 | }
181 |
182 | onDoubleClick(event) {
183 | if (this._config.enable_double_click !== undefined && !this._config.enable_double_click) return;
184 |
185 | event.stopImmediatePropagation();
186 |
187 | clearTimeout(this.timer);
188 | this.timer = null;
189 |
190 | this.sendKey(this._config.double_click_keycode ? this._config.double_click_keycode : "KEY_RETURN");
191 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "success");
192 | }
193 |
194 | onTouchStart(event) {
195 | event.stopImmediatePropagation();
196 |
197 | this.holdaction = "KEY_ENTER";
198 | this.holdtimer = setTimeout(() => {
199 | //hold
200 | this.holdinterval = setInterval(() => {
201 | this.sendKey(this.holdaction);
202 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
203 | }, 200);
204 | }, 700);
205 | window.initialX = event.touches[0].clientX;
206 | window.initialY = event.touches[0].clientY;
207 | }
208 |
209 | onTouchEnd(event) {
210 | clearTimeout(this.timer);
211 | clearTimeout(this.holdtimer);
212 | clearInterval(this.holdinterval);
213 |
214 | this.holdtimer = null;
215 | this.timer = null;
216 | this.holdinterval = null;
217 | this.holdaction = null;
218 | }
219 |
220 | onTouchMove(event) {
221 | if (!initialX || !initialY) {
222 | return;
223 | }
224 |
225 | var currentX = event.touches[0].clientX;
226 | var currentY = event.touches[0].clientY;
227 |
228 | var diffX = initialX - currentX;
229 | var diffY = initialY - currentY;
230 |
231 | if (Math.abs(diffX) > Math.abs(diffY)) {
232 | // sliding horizontally
233 | let key = diffX > 0 ? "KEY_LEFT" : "KEY_RIGHT";
234 | this.holdaction = key;
235 | this.sendKey(key);
236 | } else {
237 | // sliding vertically
238 | let key = diffY > 0 ? "KEY_UP" : "KEY_DOWN";
239 | this.holdaction = key;
240 | this.sendKey(key);
241 | }
242 |
243 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "selection");
244 | initialX = null;
245 | initialY = null;
246 | }
247 |
248 | handleActionClick(e) {
249 | let action = e.currentTarget.action;
250 | let info = this.custom_keys[action] || this.custom_sources[action] || keys[action] || sources[action];
251 |
252 | if (info.key) {
253 | this.sendKey(info.key);
254 | }
255 | else if (info.source) {
256 | this.changeSource(info.source);
257 | }
258 | else if (info.service) {
259 | const [domain, service] = info.service.split(".", 2);
260 | this._hass.callService(domain, service, info.service_data);
261 | }
262 |
263 | if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
264 | }
265 |
266 | buildIconButton(action) {
267 | let button_info = this.custom_keys[action] || this.custom_sources[action] || keys[action] || sources[action] || {};
268 | let icon = button_info.icon;
269 | let custom_svg_path = this.custom_icons[icon];
270 |
271 | return html`
272 |