From 009a9d59ed175050fa9bf5374563b8e7f232009e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 May 2017 21:38:28 -0700 Subject: [PATCH 01/21] Update frontend --- homeassistant/components/frontend/version.py | 6 +++--- .../frontend/www_static/frontend.html | 5 +++-- .../frontend/www_static/frontend.html.gz | Bin 140429 -> 140627 bytes .../www_static/home-assistant-polymer | 2 +- .../components/frontend/www_static/mdi.html | 2 +- .../frontend/www_static/mdi.html.gz | Bin 197577 -> 198311 bytes .../www_static/panels/ha-panel-hassio.html | 4 ++-- .../www_static/panels/ha-panel-hassio.html.gz | Bin 7449 -> 7451 bytes .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2518 -> 2513 bytes 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 23437de3924..943074beb40 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,8 +3,8 @@ FINGERPRINTS = { "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", "core.js": "5d08475f03adb5969bd31855d5ca0cfd", - "frontend.html": "094c2015c8291c767b8933428d92076f", - "mdi.html": "1cc8593d3684f7f6f3b3854403216f77", + "frontend.html": "5999c8fac69c503b846672cae75a12b0", + "mdi.html": "f407a5a57addbe93817ee1b244d33fbe", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1", "panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2", @@ -12,7 +12,7 @@ FINGERPRINTS = { "panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", "panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", "panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", - "panels/ha-panel-hassio.html": "0aa1523357326cb40e2242dce9b2c0d6", + "panels/ha-panel-hassio.html": "333f86e5f516b31e52365e412deb7fdc", "panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index fa6cbcd1717..1fd4bda0275 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -8,6 +8,7 @@ window.hassUtil.DEFAULT_ICON = 'mdi:bookmark'; window.hassUtil.OFF_STATES = ['off', 'closed', 'unlocked']; window.hassUtil.DOMAINS_WITH_CARD = [ + 'binary_sensor', 'climate', 'cover', 'configurator', @@ -20,7 +21,7 @@ window.hassUtil.DOMAINS_WITH_CARD = [ ]; window.hassUtil.DOMAINS_WITH_MORE_INFO = [ - 'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator', + 'alarm_control_panel', 'automation', 'binary_sensor', 'camera', 'climate', 'configurator', 'cover', 'fan', 'group', 'light', 'lock', 'media_player', 'script', 'sun', 'updater', ]; @@ -440,7 +441,7 @@ window.hassUtil.isComponentLoaded = function (hass, component) { window.hassUtil.computeLocationName = function (hass) { return hass.config.core.location_name; -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index 719b9c608f185cedf81b026331d45d7403fd9af8..c7689872442293fb47f9500bae9c7482f0077000 100644 GIT binary patch delta 2296 zcmVI-5ENABzYGYyk|h2R;;kQN;Jwb~5q!vBVRkm_vkr27kp6 z#Ofh=qGV?dO+CvyhTVJA56Rc6k?#^$24HVhal$N_UM%h2=R2~Xdad1FYWihrN$T>#ATBQ+Lt94*TfI=;L#B|U)5!eNS!|-)ENMxg zD(fkOPm*XMH0!>DM4sT#I0ut|%+d(m>%%8$FeIS9Sgt_YbwbGR{nrgTVN^Pu>c+T% zDP2{yZ@Xl)EjxFXn}if3Rrpvu^zpIxWsQ8P8qSHfvQB=Z>g(eTiHNng4{p3W>A z=0*Q@c_Eq!1>U!%;5E@$@m}BN>#TvGFhlD-t$E65Z)Z0zxGw$2IRN&5eLg+Md8do; zu8_*V+y2?RaCj)}ZZ=}j+pT(+qy_vWet~{{_CR#{9$#sW^;`!bC}LoEJ!W9QdQBB4 zAax56U6U`FqigcYLsXh*+w~BNG2pHf_X2C>;6)3i?RKg%Z8x|$Y1zK=C=cXDyw|f(^noFkWQ8my5%JxqDcI}K&TQTk{2{$m& z`FzIvIe>l4pLY1%dWr;wRwS?aXTT*tk9Hcpy2R@;uGv$67}v~hT>DtgjK!<-yk#&m zy(2cYlZ?MUqjzN`OF-kS8z$CoE~qvQe#|E#^EB_59qaLvv7<3L$wV5sIE5RNmoNFj zSw4|f7vvI;&2n}xWHOn=5?#L4y=uQ!@_W*wZI@9Pias7gNmz~>7*A3KB*sq`*MVhU z{{aJS+%|uImpLD_LF_q+dYxzUWU(%ZbF)llOO#LCc-N=Fp}8|B5a_Vz*O{03DsSumxfE7c`anlSKak#k_V(s$u6vMRJ|LQX;3Y^*r=smf}(CsQ_}o-5u2!t3Hs>2W@Yx&Ux?dT;qh??YNCf?{^zjI2_b^AC=zw zKB}g2o=g?(-d}jw7C{Fa$Z#GHSpK<#d2zGU;qFh>M%3KIs+HZtLFiR2WR> zB}&6?8d0hRcORw6nn|6BUz(A*rmOLzY@=d-=1Q|#FzYbs%w#cH@+@(5jqi!$=&Kn> z8S{5X7aQyyV^EMspIw)7e^@6e-p`P%;rgJUKrvEURHGq#%_&e%DupV_a{XF?jT*0M zp-e6FvU0bVzUBOwb^Zmzc)L@#%i-G{(?2qXuDngfj)9X@KG3H9<`i?ay&c!dbRhYE z`99mpnzt@iZs#s?HQ`SZh;P2w&_>yl$%}%UP~FZVIuP)>oO%1h>1uLusUwF)=nAXf z=_fvwnU*eM=Q;2&@VeDx)`A70GhT`F4NbOZE}^Y`GR0d$mUmwpt7Z=C9}}aC-!?NZ z^WEFh?)^O){nC^1z8BX~vJ573HoyvhCvd*|24BXpz0t1!7U}=#y9=ff(dUVA1LiI`IozvL{o%+xy=$OWGR#+`SY+D%6_6++X~27zuN$5+MOZ`&QW+}E^7Np_ z{0t+Lf|lQ^t&zLgn104~3-HEJ07O26!7D3XCf1Pe2WlPf+=o!_ew;I7LgK-UKUDf& z{>0GER8#(Yg(68(S36b(+#2_PE`_XP>zmKnCP$60)%kragwuG5#`~dyD7q^7$DL8uyV?7n;1NG(z2gxc0kPv59u6^o&&wHko9n+F z*8Owf`%-a)!4;4E%m!cKu%Qk0&E9+O=nyt+X<#s_Dg?t zXyoFTa~|s6KV-z8rnZNS&t_I$=?iCpQ6LVgIpdsi*Z?g zKTOfY0QqsOwg&R{rkrYLdD&Ke*B+w7Yn*3|+9o=`h2Pcl0P65>LcP(T1#BSq_Z>P< zXydTi;{wT+u;s*9h~n#x?*%q&uH~#pn+KRPqE7lXRcLFaccit`twVP+HfgMt;TegG SDNDLaPyZJH4VrF!g#Z9V6QX(m delta 2294 zcmVm`v#azDr;kfW1}43A5ySvFvwS6U>YMQkhu?N%di! zW&B?Gy;qSt==B<&@5qDdwRU@{>6fb|smlw4xV(4_Z5>H$^+I_MnL>_EBlAn=v59W8 zq$Po>tfveh` z-*=lldlwFmf8EVS40^j&kCC*1pTrf=@5~;EPT%9x%(0%}AY?-f4DZAY3|OzJ)dZxj z^rLI?B@=T^UU`TX6K%U*KQRW}bn~AV26fS!>lg0}dGF_}B z^O96c%!Owkn(tCPrLeIXzQ3e3IlX>;NXLlNHI= zbkN**(Xjy%e>oyO-8A7~ICTl+W0-FA0DsOz^`SoH_I%qFgRldsZ=!bzA^Ize7< z#zzi+D#q_*d6q$1=Ba$m20?tAD&E+VLH|GfR8QI5l@Q7j_t@iq+I`|^_GxEEXgdQ{ z-=;G1!JjaBwBC+?YIzxTmD(3*rD4`E4UEh`x1&_pc6Z`vh%6fu!*+L4Q9f*ULx@Im zYJ^1e9!fuY1=dDisABea)Wn6Yx*rvKnygh#NqwQT)%%#79VxDzVQDL@eI?-rCOV(b zcs~cQkNMM1oLldYz|e~1HUA8_ejIi*CMTK5{T4@WWAgGPKP}5A zvg(3d;#pbFE`v-alUSn5x4KvD*Ghg*PPFX?3PaJyV<-trYy;y-s({4!$>KV&lPjEzi0G%#+uq)Pe9d(a63hogvk$!ah}nCE^e%hd zLO%yX{svv80k-t^9TIipV;!Tz%M&6I(a-W*>`ZSR3rN`Wuz@vPmnowza`4^sMzOx)P?&)s{ESRn?Zpaiqp|*&2Ki0Vas;IhK^v`fvbjLCAm%ccv%Z;I1u-xY6 z8QPU;`P>;{nS*cQ7xmlRDh}sd`*1mAit?ZNewLSK4jeTb>Go>AX2< z*i9o!wczffG+8sLGx19^GS_r9ew1xg%v@=IRtshwCY_lqCQF_rj;`@NaU6X$11V$v z?&xBJy<-dt^60ZWQtl7yB*jY^k~Lf(6ci{%N{ebVWKTE+>K&y}MOm(2E3i@H2`!YV zWnNb9_R_Z;8MDs6U>I+A>UKGNyJPxC#?Y0wsY1Nvm89}jHtmO|n5*sWxI?A`$lZ#6oIV?g~Sp7~v z@u|#QbP+qxfro+DttPV;EC`+P9-MDzvORMNZS9jO9uTrT_S#rAb6EeF7+w6JnR$cn z-j;UnkI?8hos37lxQ>!#FqyLfRycuw^W8W29FFZ_cKruP|4$!VFpZc-p|tes+`uv3 zr+>gB><@T(-B{m;s8Rj0{vLh0%1>d1i zdz{X(y_|EyB&bUQSoM>5zQ8OLZx$e83xWsgZ0k0rbn0uk32kj-N-`v|_aT6PJ?^`+ zYlPUWl~-n)F#@RTXSB3h8jP-&L;1~ukq z7?~8b{8nv^+|9=HGqziRH--Wr^6d*=S@ANlhI~Iz>v-orgnI1boEZ}m4`%#z()aRr zg?46_@*gP_d6Bx>u`1x!xOXXkWF1@I6wWp|YJ9EE?_(jH#`81Q$7&q)s7&M88Am+@ z)A?A8BGXj${qv!JOUO;v5<2WX(^bK&OzXI82RA-sta}dwvCn&^dV@RJ&7SSZfk~RN zt?-Z`whwUgl5{Okub)HFRmnf@jI!R%-v927_zmkFFYgG59dGY&hGT~*kMHRH;sL3~F4k$OA3FkaqN*2QJJ@SF<1*P!Fs-s*`cp$A z7r&hIQ1|{JBmN$>J!E_?gDvI5G$zvcfmlEC^TuN|c)3pgP_X(G9UdO!^1xY)%kraP ziY5lgk7KnpkXJV4RLdJ>w(`67`W)WfJZsc8(fKX>uAT=_hkq05jRq}X1G&HN(0M`| zhs_=rNVbG6C&oe)Uw3>Yuwip8XFb|Hz?>0v(yysPTPwXIt(|Tix|^{{W33F&NZdtP Q(p7r;zk*1?8FGaH0GK0?AOHXW diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 244c0a6b13d..79fc54a2fc6 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","43b9f99469029c48e429731761b03abd"],["/frontend/panels/dev-event-2db9c218065ef0f61d8d08db8093cad2.html","b5b751e49b1bba55f633ae0d7a92677d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-415552027cb083badeff5f16080410ed.html","a4b1ec9bfa5bc3529af7783ae56cb55c"],["/frontend/panels/dev-state-d70314913b8923d750932367b1099750.html","c61b5b1461959aac106400e122993e9e"],["/frontend/panels/dev-template-567fbf86735e1b891e40c2f4060fec9b.html","d2853ecf45de1dbadf49fe99a7424ef3"],["/frontend/panels/map-31c592c239636f91e07c7ac232a5ebc4.html","182580419ce2c935ae6ec65502b6db96"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-5d08475f03adb5969bd31855d5ca0cfd.js","1cd99ba798bfcff9768c9d2bb2f58a7c"],["/static/frontend-094c2015c8291c767b8933428d92076f.html","9f6ff2fa80d6e34106c533b941ad0141"],["/static/mdi-1cc8593d3684f7f6f3b3854403216f77.html","eac41ec8397af607a07bc174e3c2475f"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;nRRMwlCb**gK)T0jUbMs871?V?z)%g(IwP4q6J%Z>H+;)Bt*#*@8cEQ01OR+lr zV%=8v!TtHAHQbC|fyEj8Y-dZ|*wU0rxQCyvp|;ONRY{>f`4HOR&Xg^@Sa*xv?49V> zUD;?Dgj!gEPy2ErHy2tle>{&mP8+K?4D^yx_gT>RA2%3yIm2+v)Syq z{0Pbp=nb!esDgTK7Z(?sPgPT+C)Ua~?QX%v#eulcfK?0Mn}}68>CL;17Is;&iQ3rU zg?CG^i$-60FP^`8?S%^v8rts4q5Jn??B99Mn%W|k!295yXz98R3izp-7yiaSS9$IH$njydDe7RpnH%EH}~=Sxy-cREKVe zRbtXOi;dgDR+UZ)MnRdBX&@T6Zc=W5bCIV>3WlCmg=uAsXfvLugz}7L1{K7)$^||r zf`L@&Rs$&#=5XW+62&PKkb%k=ixZh4k+MO91E^FJ)ht&ykY!cc+i^iPS9zYNg%(C| zEmJN4iIW?u3nSy`V}}ZuiJ3yTK_cU%&@9h+mKgkFJWi3Mj}){B4$?KvaUB&}GM;g% zDJdA!Oo><-J;)_Wb1p^B3u%-wJkJWnbs}YASRrzCAa{$=eW}YaQL12^YMK=(@;Wof@PegjOcRnJ z54VCrs1#XRP@W5u#kq)csYspzRf*elFL-M;8r@CWMmDyYyzGC*KQ@()tToO?nsC&L zM&cd}A*tXjO%fr5CNj?Gw?zF?&KGtgE0@g3qA-X78b?xUkkJXjI7*U)BD;~Z&~7{!W=#RRF5OQdo7O~YQod|iolBTXo3a?UlwEM*wsnGq7Rjx013 zDbjpjTImYI#E5G*7!#g+#=mG)&ruF?ocA72knx$wAt_?qp#&ZiLUMLiPK5t(eVitg zNiA~J3T|7-1m$RrT*@#+57sZm96GEtt^Tk?Xv7DM`&(!~$oa+^LDQrlnkZ6GrYS8% zmL_Nlr!izih<#}{7_b<5O^xZj8)NKY=TO!|7|=vA!DTMAC^8zS=)O#*$U~D5o|@Qu z|32)C%x_`|Gq1ZDZ>!GunvTcbrQbniq88>QR;a#&Jw2LCo3;{ljuzN0?+5HA;^|dly{cZ`DXYc-(~9gC=aN(zoH=wtawT zGYXym)^1`LY@T1kXSl>czQ?%8|)09?tIrZOE}pFZ}!%=RH19JRzLRy)dvjy zW9&m|10u8SV8cHWL>)GL)<>j*^G*Bao0;c2cT-lV`P7Ft!al9NJ;A6an2n*suKr#8 zKIr@Zv${SuWxZ83jM215F|iQcqQ_te{SSMw?}d26=w3HlYIQvtzoNFop7h872c4Zc z1H&4B2!{HD=fXa5RL#&GBR^(`1op+DO~~OVWQ+bgv1fd!-ronow3mmuq(993;z(5^ zrWm&vI~7*beaj%a5v6@o+p_Ze9dr@qtlHltzw@W5<1^gZnBPVzs`r0rTX7pyN>!!n1n^gs%Wpu{GvyE`w8Hz_ zPp`c`8Mgx~qTwXG0TE;SY%cX5*5DSt)(-?zJA3Sff%sF4rKp}AbqUB93~n3@-Et6~ z6A(-eeUFbnEm#%+1YJ`)?H}~!KdG1=>6}6917rL z`E%$?^zk}$KI~{W9g@T*LA+kTs8^{U(t%wFJGv3w2tKd+-f2{JBV>4o8;iVws;al( zL=JFGbW^!-Yis}eXU)3O^}oKiBZoIU=;#yDcbzwKsdTX5YV_Un?+@|DOtQJs*-iJ% zKTrMDx?7yc5Yyp)2;GN;Y;*I}xfl!g6T^ufc+uSaws+0tW3?`Av*f@Zv?%5jWYz?#t-iARMlW| zKbjs;J2C0pvw`#EyTGm5%ekBM)id`8Oq3gJZZPzl8}HP$XWy}fVv1D3po^#VU$$-J zw7$FykkkUZks7-Zrw_A%%MPYd>&`X#VoP`t^>cnO#T8pKT&TV6RR_Bw_xO!(zn*0e boq^~bI^(Nuz^{8RV0Hg5e3-=mV-^4aUv%DK literal 2518 zcmV;{2`Tm;iwFqBD+pNv19N3^c4=c}Uw3bEYh`jSYI6XkSZj0Jx)J>=3a8g0Yl&$$K*qro55otF`-D6gzUJE|9clCB}wXt_l|f&Q-{WST3x}^(<*?Do*S4-4|JOAN zX^Q`Ja~IBUz#C{r&bhhud{L|Gi+{X({VK4?spe=!AGAx}j8+XuiyYLTi}~!+CuidL zP7({I8B0J>C94-6|bOdCkigdU}HXxlwdL1QI5Q5S&t-#xaLbWt>H6mOY@SsB}F}NlHQp3FQeP zQp-F6r6QTABxjHjwNaDOG>@|kASP0%EZ3l97{&s3q%+O+X*Df0mxY9JLMWwC7-dp~ zc`Pt4T2oEZFb@+nVXKKJf`DW~b1Gy^BgS=><+vxBNo(m2(qAk1XO z@j2oYgiJOXNFGt^M2BB*G|HG|N~TY5b!sOc12^6g21@glm%Fx|l0L zS;~ZpNlvMvl7~{OUM`V}F~KvI3oW%~S(;0xA|WD8bDqgPxf>6SLzc)0K`Zl!5t(6d za~8)58pfh9OZCPrOs85$n&+WPAx4;`lE#>r3E?Uv3E7jrRK=J`nNt?4IL#Bpb*d5J zIZcu9d!!hBuwW{}2ZtTVz8~EEq0nleOaPw_!rFkYiPcR`CtvV zpd*S%lE+lVan92uLQ_~ALP|K_m3D;*i9Xg*s`(2ZH#g{O1+N<%V>x?hs@z^1a^M*Gl(O{2!$+6L5Z<~FZ{Ju8JFO$zcSL(0W@oj&;eqJf~KD#3^w(;BU zSIumn{qXh$HpFSqYq0-eJFfyPGK&hO2Q9W*aL2?uJ9F=*&u7N-k%83jx}NHKx4tZa zS8t(3C$%s;%BdQRgEa}%6W6hQ*(q@P$(yEL7Oj2V*5xH2;d=K;nAsy? z8x7!GJ&F1uc?3iSw2)kal6M$_))#Dcer&If(!f7!`>$Hx;!`@Y^ltL?n;61DAf z(gOpm>&(m==vVkdFxBrO=XZsptb2Bh_?Ybj*yZ~+A%-83jrTwC9pioZ`PTEMojj~1 z-C^d11yzMU#k|GbDY2XGS_Z)tFU*I^6s6nkpbfBQRqnR@6}R%^Q((*gMwqj}^&yAuHnbo;GXeZ&~{@U;v469BNs z)k_4{J_34dZjQz$Lo)q8kaFToUme9q^-%Eb5#RH+V*8^je)C5hCCkDx0sQ5~@+Yq9 zkh1S5w8Ht@v-eI{jGG=6L4Oh+oQS!7vX;7cYj6t>@d0mYX7{7e1Al6;6;-nXmVkJ{ z9=ScM`oUMl4_m9sAHzZO z%T?Wcg9drkk0q}PZDrXLK4!7^=RYAK+uM0ZNw&4mp4|7V-)=QKxeedgD~&$+)JIIc z{~eHh1H3PP4qb~rT!+DjE$pU!kr)cZ`vr_TmFlh>m<2bZE8dRa`>NwkqoN%l!du)} z;Pg#W1Ap1u)ux;54iup8gI zzmsK+#r5<0(STR0$31|X=4@u09OC&XaE2J From 81b567b68f8e43b9a9d4fba1b22f27d505168084 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:44:33 +0200 Subject: [PATCH 02/21] LIFX: refresh state after stopping an effect This clears the internal cache in case polling picked up the state as set by an effect. For example, aborting an effect by selecting a new brightness could keep a color set by the effect. --- homeassistant/components/light/lifx/effects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 07b97d03a12..2c054d49e1a 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -205,6 +205,7 @@ class LIFXEffect(object): light.device.set_color(light.effect_data.color) yield from asyncio.sleep(0.5) light.effect_data = None + yield from light.refresh_state() self.lights.remove(light) def from_poweroff_hsbk(self, light, **kwargs): From 655b82c1e215874353db66e32c18ca94618ecd46 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:47:32 +0200 Subject: [PATCH 03/21] LIFX: Use 3500K as neutral white This does not really matter because the colorloop uses saturated colors (without much white). Anyway, just copy the 3500K that the LIFX app uses. --- homeassistant/components/light/lifx/effects.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 2c054d49e1a..aa5179e0207 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -31,6 +31,8 @@ ATTR_CHANGE = 'change' WAVEFORM_SINE = 1 WAVEFORM_PULSE = 4 +NEUTRAL_WHITE = 3500 + LIFX_EFFECT_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, @@ -313,7 +315,7 @@ class LIFXEffectColorloop(LIFXEffect): int(65535/359*lhue), int(random.uniform(0.8, 1.0)*65535), brightness, - 4000, + NEUTRAL_WHITE, ] light.device.set_color(hsbk, None, transition) @@ -325,7 +327,7 @@ class LIFXEffectColorloop(LIFXEffect): def from_poweroff_hsbk(self, light, **kwargs): """Start from a random hue.""" - return [random.randint(0, 65535), 65535, 0, 4000] + return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] class LIFXEffectStop(LIFXEffect): From 1e4b56c4d4300310109cf1e49c1c98dc707469bf Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:51:33 +0200 Subject: [PATCH 04/21] LIFX: Move random hue initial color to the LIFXEffect base class It's a reasonable default for several light effects. --- homeassistant/components/light/lifx/effects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index aa5179e0207..1365be60d71 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -212,7 +212,7 @@ class LIFXEffect(object): def from_poweroff_hsbk(self, light, **kwargs): """Return the color when starting from a powered off state.""" - return None + return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] class LIFXEffectBreathe(LIFXEffect): @@ -325,10 +325,6 @@ class LIFXEffectColorloop(LIFXEffect): yield from asyncio.sleep(period) - def from_poweroff_hsbk(self, light, **kwargs): - """Start from a random hue.""" - return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] - class LIFXEffectStop(LIFXEffect): """A no-op effect, but starting it will stop an existing effect.""" From 4be89521d4879023aaa095674175c486fcf68f05 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 29 Apr 2017 18:27:22 +0200 Subject: [PATCH 05/21] LIFX: Update aiolifx requirement This update silences some warnings (frawau/aiolifx#7). --- homeassistant/components/light/lifx/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index f1b20f904d2..22e7ee04fcc 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -32,7 +32,7 @@ from . import effects as lifx_effects _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.4.5'] +REQUIREMENTS = ['aiolifx==0.4.6'] UDP_BROADCAST_PORT = 56700 diff --git a/requirements_all.txt b/requirements_all.txt index b375c904113..4f0edd805e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -48,7 +48,7 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.4.5 +aiolifx==0.4.6 # homeassistant.components.alarmdecoder alarmdecoder==0.12.1.0 From 1c5eb8836801173e7f5dc7807edcd7a171e7e6d0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 29 Apr 2017 22:24:18 +0200 Subject: [PATCH 06/21] LIFX: avoid warnings about already running updates Forcing a refresh will log a warning if the periodic async_update happens to be running already. So let's do the refresh locally and remove the force_refresh. --- homeassistant/components/light/lifx/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 22e7ee04fcc..01038814f51 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -263,17 +263,19 @@ class LIFXLight(Light): """Return the list of supported effects.""" return lifx_effects.effect_list() - @callback + @asyncio.coroutine def update_after_transition(self, now): """Request new status after completion of the last transition.""" self.postponed_update = None - self.hass.async_add_job(self.async_update_ha_state(force_refresh=True)) + yield from self.refresh_state() + yield from self.async_update_ha_state() - @callback + @asyncio.coroutine def unblock_updates(self, now): """Allow async_update after the new state has settled on the bulb.""" self.blocker = None - self.hass.async_add_job(self.async_update_ha_state(force_refresh=True)) + yield from self.refresh_state() + yield from self.async_update_ha_state() def update_later(self, when): """Block immediate update requests and schedule one for later.""" From 71f9507df724d11fbf5fd1776f40813fdd0dddd0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 2 May 2017 23:30:07 +0200 Subject: [PATCH 07/21] LIFX: fix color restore after running effects State restoration takes up to a second because bulbs can be slow to react. During this time an effect could keep running, overwriting the state that we were trying to restore. Now the effect forgets the light immediately and it thus avoids further changes while the restored state settles. --- .../components/light/lifx/effects.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 1365be60d71..2dc56443723 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -198,18 +198,19 @@ class LIFXEffect(object): @asyncio.coroutine def async_restore(self, light): """Restore to the original state (if we are still running).""" - if light.effect_data: - if light.effect_data.effect == self: - if light.device and not light.effect_data.power: - light.device.set_power(False) - yield from asyncio.sleep(0.5) - if light.device: - light.device.set_color(light.effect_data.color) - yield from asyncio.sleep(0.5) - light.effect_data = None - yield from light.refresh_state() + if light in self.lights: self.lights.remove(light) + if light.effect_data and light.effect_data.effect == self: + if light.device and not light.effect_data.power: + light.device.set_power(False) + yield from asyncio.sleep(0.5) + if light.device: + light.device.set_color(light.effect_data.color) + yield from asyncio.sleep(0.5) + light.effect_data = None + yield from light.refresh_state() + def from_poweroff_hsbk(self, light, **kwargs): """Return the color when starting from a powered off state.""" return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] From bfd61100914df10241519d828ca3fc54a4b92039 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 3 May 2017 21:26:04 +0200 Subject: [PATCH 08/21] LIFX: handle unavailable lights gracefully Recent aiolifx allow sending messages to unregistered devices (as a no-op). This is handy because bulbs can disappear anytime we yield and constantly testing for availability is both error-prone and annoying. So keep the aiolifx device around until a new one registers on the same mac_addr. --- .../components/light/lifx/__init__.py | 19 +++++++------ .../components/light/lifx/effects.py | 27 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 01038814f51..f13934011e9 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -93,6 +93,7 @@ class LIFXManager(object): if device.mac_addr in self.entities: entity = self.entities[device.mac_addr] entity.device = device + entity.registered = True _LOGGER.debug("%s register AGAIN", entity.who) self.hass.async_add_job(entity.async_update_ha_state()) else: @@ -118,7 +119,7 @@ class LIFXManager(object): if device.mac_addr in self.entities: entity = self.entities[device.mac_addr] _LOGGER.debug("%s unregister", entity.who) - entity.device = None + entity.registered = False self.hass.async_add_job(entity.async_update_ha_state()) @@ -172,6 +173,7 @@ class LIFXLight(Light): def __init__(self, device): """Initialize the light.""" self.device = device + self.registered = True self.product = device.product self.blocker = None self.effect_data = None @@ -183,7 +185,7 @@ class LIFXLight(Light): @property def available(self): """Return the availability of the device.""" - return self.device is not None + return self.registered @property def name(self): @@ -345,7 +347,7 @@ class LIFXLight(Light): def async_update(self): """Update bulb status (if it is available).""" _LOGGER.debug("%s async_update", self.who) - if self.available and self.blocker is None: + if self.blocker is None: yield from self.refresh_state() @asyncio.coroutine @@ -357,11 +359,12 @@ class LIFXLight(Light): @asyncio.coroutine def refresh_state(self): """Ask the device about its current state and update our copy.""" - msg = yield from AwaitAioLIFX(self).wait(self.device.get_color) - if msg is not None: - self.set_power(self.device.power_level) - self.set_color(*self.device.color) - self._name = self.device.label + if self.available: + msg = yield from AwaitAioLIFX(self).wait(self.device.get_color) + if msg is not None: + self.set_power(self.device.power_level) + self.set_color(*self.device.color) + self._name = self.device.label def find_hsbk(self, **kwargs): """Find the desired color from a number of possible inputs.""" diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 2dc56443723..a15360df33e 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -176,18 +176,16 @@ class LIFXEffect(object): def async_setup(self, **kwargs): """Prepare all lights for the effect.""" for light in self.lights: + # Remember the current state (as far as we know it) yield from light.refresh_state() - if not light.device: - self.lights.remove(light) - else: - light.effect_data = LIFXEffectData( - self, light.is_on, light.device.color) + light.effect_data = LIFXEffectData( + self, light.is_on, light.device.color) - # Temporarily turn on power for the effect to be visible - if kwargs[ATTR_POWER_ON] and not light.is_on: - hsbk = self.from_poweroff_hsbk(light, **kwargs) - light.device.set_color(hsbk) - light.device.set_power(True) + # Temporarily turn on power for the effect to be visible + if kwargs[ATTR_POWER_ON] and not light.is_on: + hsbk = self.from_poweroff_hsbk(light, **kwargs) + light.device.set_color(hsbk) + light.device.set_power(True) # pylint: disable=no-self-use @asyncio.coroutine @@ -202,12 +200,13 @@ class LIFXEffect(object): self.lights.remove(light) if light.effect_data and light.effect_data.effect == self: - if light.device and not light.effect_data.power: + if not light.effect_data.power: light.device.set_power(False) yield from asyncio.sleep(0.5) - if light.device: - light.device.set_color(light.effect_data.color) - yield from asyncio.sleep(0.5) + + light.device.set_color(light.effect_data.color) + yield from asyncio.sleep(0.5) + light.effect_data = None yield from light.refresh_state() From 03e3fb77c4140092ed298852e2cbbab1405a05d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 May 2017 21:41:11 -0700 Subject: [PATCH 09/21] Version bump to 0.44 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5ab322fab6c..d4014a7f161 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 44 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 94f7c397d74dcc5a4081ff6b94219c81a6fce07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 5 May 2017 08:50:53 +0200 Subject: [PATCH 10/21] Add hass to rfxtrx object (#6844) --- homeassistant/components/cover/rfxtrx.py | 4 ++-- homeassistant/components/light/rfxtrx.py | 4 ++-- homeassistant/components/rfxtrx.py | 6 ++++-- homeassistant/components/switch/rfxtrx.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index 0e28d3ef701..f599ea3ede1 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the RFXtrx cover.""" import RFXtrx as rfxtrxmod - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) + covers = rfxtrx.get_devices_from_config(config, RfxtrxCover, hass) add_devices_callback(covers) def cover_update(event): @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): not event.device.known_to_be_rollershutter: return - new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) + new_device = rfxtrx.get_new_device(event, config, RfxtrxCover, hass) if new_device: add_devices_callback([new_device]) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 9248b0131f1..f831d6c04ce 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the RFXtrx platform.""" import RFXtrx as rfxtrxmod - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) + lights = rfxtrx.get_devices_from_config(config, RfxtrxLight, hass) add_devices(lights) def light_update(event): @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): not event.device.known_to_be_dimmable: return - new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) + new_device = rfxtrx.get_new_device(event, config, RfxtrxLight, hass) if new_device: add_devices([new_device]) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index b7f016d1029..3c3f1e00f68 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -191,7 +191,7 @@ def get_rfx_object(packetid): return obj -def get_devices_from_config(config, device): +def get_devices_from_config(config, device, hass): """Read rfxtrx configuration.""" signal_repetitions = config[CONF_SIGNAL_REPETITIONS] @@ -209,12 +209,13 @@ def get_devices_from_config(config, device): new_device = device(entity_info[ATTR_NAME], event, datas, signal_repetitions) + new_device.hass = hass RFX_DEVICES[device_id] = new_device devices.append(new_device) return devices -def get_new_device(event, config, device): +def get_new_device(event, config, device, hass): """Add entity if not exist and the automatic_add is True.""" device_id = slugify(event.device.id_string.lower()) if device_id in RFX_DEVICES: @@ -235,6 +236,7 @@ def get_new_device(event, config, device): signal_repetitions = config[CONF_SIGNAL_REPETITIONS] new_device = device(pkt_id, event, datas, signal_repetitions) + new_device.hass = hass RFX_DEVICES[device_id] = new_device return new_device diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 1361d22de18..36044f5f168 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import RFXtrx as rfxtrxmod # Add switch from config file - switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) + switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch, hass) add_devices_callback(switches) def switch_update(event): @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): event.device.known_to_be_rollershutter: return - new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch) + new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch, hass) if new_device: add_devices_callback([new_device]) From abe6f9343ffde31f145e618c15a0402654fdefa8 Mon Sep 17 00:00:00 2001 From: Gergely Imreh Date: Fri, 5 May 2017 19:37:54 +0100 Subject: [PATCH 11/21] sensor.envirophat: add missing requirement (#7451) Adding requirements that is not explicitly pulled in by the library that manages the Enviro pHAT. --- homeassistant/components/sensor/envirophat.py | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index 0c4bb42cf8f..48370d76c83 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -15,7 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['envirophat==0.0.6'] +REQUIREMENTS = ['envirophat==0.0.6', + 'smbus-cffi==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4f0edd805e1..3dd416f3b67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,6 +764,9 @@ sleekxmpp==1.3.2 # homeassistant.components.sleepiq sleepyq==0.6 +# homeassistant.components.sensor.envirophat +smbus-cffi==0.5.1 + # homeassistant.components.media_player.snapcast snapcast==1.2.2 From e2559fd6cf80fc981b45ed3a7a4b25eaa9eedb8a Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Fri, 5 May 2017 19:19:24 -0400 Subject: [PATCH 12/21] Fix object type for default KNX port #7429 describes a TypeError that is raised if the port is omitted in the config for the KNX component (integer is required (got type str)). This commit changes the default port from a string to an integer. I expect this will resolve that issue... --- homeassistant/components/knx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index f72a3048dec..ff951e55810 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -18,7 +18,7 @@ REQUIREMENTS = ['knxip==0.3.3'] _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = '0.0.0.0' -DEFAULT_PORT = '3671' +DEFAULT_PORT = 3671 DOMAIN = 'knx' EVENT_KNX_FRAME_RECEIVED = 'knx_frame_received' From 1ba44356936e9a3cda93af753a682eac9ecb2061 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Mon, 1 May 2017 13:12:43 -0400 Subject: [PATCH 13/21] repairing functionality for non-zero based ranges --- homeassistant/components/cover/mqtt.py | 52 +++++++++++++++--- tests/components/cover/test_mqtt.py | 73 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 829c3748d2f..9d97851b5b4 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -40,6 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value' CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' +CONF_TILT_INVERT_STATE = "tilt_invert_state" DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -52,6 +53,7 @@ DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_MIN = 0 DEFAULT_TILT_MAX = 100 DEFAULT_TILT_OPTIMISTIC = False +DEFAULT_TILT_INVERT_STATE = False TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | SUPPORT_SET_TILT_POSITION) @@ -74,6 +76,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT_INVERT_STATE, + default=DEFAULT_TILT_INVERT_STATE): cv.boolean, }) @@ -104,6 +108,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_TILT_MIN), config.get(CONF_TILT_MAX), config.get(CONF_TILT_STATE_OPTIMISTIC), + config.get(CONF_TILT_INVERT_STATE), )]) @@ -114,7 +119,8 @@ class MqttCover(CoverDevice): tilt_status_topic, qos, retain, state_open, state_closed, payload_open, payload_close, payload_stop, optimistic, value_template, tilt_open_position, - tilt_closed_position, tilt_min, tilt_max, tilt_optimistic): + tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, + tilt_invert): """Initialize the cover.""" self._position = None self._state = None @@ -138,6 +144,7 @@ class MqttCover(CoverDevice): self._tilt_min = tilt_min self._tilt_max = tilt_max self._tilt_optimistic = tilt_optimistic + self._tilt_invert = tilt_invert @asyncio.coroutine def async_added_to_hass(self): @@ -150,8 +157,8 @@ class MqttCover(CoverDevice): """Handle tilt updates.""" if (payload.isnumeric() and self._tilt_min <= int(payload) <= self._tilt_max): - tilt_range = self._tilt_max - self._tilt_min - level = round(float(payload) / tilt_range * 100.0) + + level = self.find_percentage_in_range(float(payload)) self._tilt_value = level self.hass.async_add_job(self.async_update_ha_state()) @@ -278,7 +285,8 @@ class MqttCover(CoverDevice): def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_open_position, self._qos, self._retain) + self._tilt_open_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_open_position self.hass.async_add_job(self.async_update_ha_state()) @@ -287,7 +295,8 @@ class MqttCover(CoverDevice): def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_closed_position, self._qos, self._retain) + self._tilt_closed_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_closed_position self.hass.async_add_job(self.async_update_ha_state()) @@ -301,9 +310,36 @@ class MqttCover(CoverDevice): position = float(kwargs[ATTR_TILT_POSITION]) # The position needs to be between min and max - tilt_range = self._tilt_max - self._tilt_min - percentage = position / 100.0 - level = round(tilt_range * percentage) + level = self.find_in_range_from_percent(position) mqtt.async_publish(self.hass, self._tilt_command_topic, level, self._qos, self._retain) + + def find_percentage_in_range(self, position): + """Find the 0-100% value within the specified range.""" + # the range of motion as defined by the min max values + tilt_range = self._tilt_max - self._tilt_min + # offset to be zero based + offset_position = position - self._tilt_min + # the percentage value within the range + position_percentage = float(offset_position) / tilt_range * 100.0 + if self._tilt_invert: + return 100 - position_percentage + else: + return position_percentage + + def find_in_range_from_percent(self, percentage): + """Find the adjusted value for 0-100% within the specified range.""" + # if the range is 80-180 and the percentage is 90 + # this method would determine the value to send on the topic + # by offsetting the max and min, getting the percentage value and + # returning the offset + offset = self._tilt_min + tilt_range = self._tilt_max - self._tilt_min + + position = round(tilt_range * (percentage / 100.0)) + position += offset + + if self._tilt_invert: + position = self._tilt_max - position + offset + return position diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index b2dcf8e175d..e685a51f56c 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -4,6 +4,7 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN import homeassistant.components.cover as cover +from homeassistant.components.cover.mqtt import MqttCover from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) @@ -450,3 +451,75 @@ class TestCoverMQTT(unittest.TestCase): self.assertEqual(('tilt-command-topic', 25, 0, False), self.mock_publish.mock_calls[-2][1]) + + def test_find_percentage_in_range_defaults(self): + """Test find percentage in range with default range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) + + def test_find_percentage_in_range_altered(self): + """Test find percentage in range with altered range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) + + def test_find_percentage_in_range_defaults_inverted(self): + """Test find percentage in range with default range but inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) + + def test_find_percentage_in_range_altered_inverted(self): + """Test find percentage in range with altered range and inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) + + def test_find_in_range_defaults(self): + """Test find in range with default range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) + + def test_find_in_range_altered(self): + """Test find in range with altered range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) + + def test_find_in_range_defaults_inverted(self): + """Test find in range with default range but inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) + + def test_find_in_range_altered_inverted(self): + """Test find in range with altered range and inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) From 0c94df9fcfd56561e27f8dc1485009b2c79ab115 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Mon, 1 May 2017 13:34:34 -0400 Subject: [PATCH 14/21] fixing documentation --- homeassistant/components/cover/mqtt.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9d97851b5b4..91dddbfc70a 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -329,11 +329,14 @@ class MqttCover(CoverDevice): return position_percentage def find_in_range_from_percent(self, percentage): - """Find the adjusted value for 0-100% within the specified range.""" - # if the range is 80-180 and the percentage is 90 - # this method would determine the value to send on the topic - # by offsetting the max and min, getting the percentage value and - # returning the offset + """ + Find the adjusted value for 0-100% within the specified range. + + if the range is 80-180 and the percentage is 90 + this method would determine the value to send on the topic + by offsetting the max and min, getting the percentage value and + returning the offset + """ offset = self._tilt_min tilt_range = self._tilt_max - self._tilt_min From 9b920b3b40c34382c7c649e5678caeb74eeb0283 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Tue, 2 May 2017 15:41:45 -0400 Subject: [PATCH 15/21] fixing nits --- homeassistant/components/cover/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 91dddbfc70a..d44d011bcb1 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -40,7 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value' CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' -CONF_TILT_INVERT_STATE = "tilt_invert_state" +CONF_TILT_INVERT_STATE = 'tilt_invert_state' DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' From fcdfebefd9d452952d16669c1257292865d7405b Mon Sep 17 00:00:00 2001 From: pezinek Date: Sat, 6 May 2017 19:11:31 +0200 Subject: [PATCH 16/21] Forecasts for weather underground (#7062) --- .../components/sensor/wunderground.py | 708 +++++++++++++++--- tests/components/sensor/test_wunderground.py | 72 +- 2 files changed, 666 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index d50f6b0897c..4d684f405f8 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -14,13 +14,13 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, TEMP_FAHRENHEIT, TEMP_CELSIUS, - STATE_UNKNOWN, ATTR_ATTRIBUTION) + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, + STATE_UNKNOWN, ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/' -_ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/' +_RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/' _LOGGER = logging.getLogger(__name__) CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" @@ -29,50 +29,562 @@ CONF_LANG = 'lang' DEFAULT_LANG = 'EN' -MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15) -MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + + +# Helper classes for declaring sensor configurations + +class WUSensorConfig(object): + """WU Sensor Configuration. + + defines basic HA properties of the weather sensor and + stores callbacks that can parse sensor values out of + the json data received by WU API. + """ + + def __init__(self, friendly_name, feature, value, + unit_of_measurement=None, entity_picture=None, + icon="mdi:gauge", device_state_attributes=None): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + feature (string): WU feature. See: + https://www.wunderground.com/weather/api/d/docs?d=data/index + value (function(WUndergroundData)): callback that + extracts desired value from WUndergroundData object + unit_of_measurement (string): unit of meassurement + entity_picture (string): value or callback returning + URL of entity picture + icon (string): icon name or URL + device_state_attributes (dict): dictionary of attributes, + or callable that returns it + """ + self.friendly_name = friendly_name + self.unit_of_measurement = unit_of_measurement + self.feature = feature + self.value = value + self.entity_picture = entity_picture + self.icon = icon + self.device_state_attributes = device_state_attributes or {} + + +class WUCurrentConditionsSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for current conditions.""" + + def __init__(self, friendly_name, field, icon="mdi:gauge", + unit_of_measurement=None): + """Constructor. + + Args: + friendly_name (string|func): Friendly name of sensor + field (string): Field name in the "current_observation" + dictionary. + icon (string): icon name or URL, if None sensor + will use current weather symbol + unit_of_measurement (string): unit of meassurement + """ + super().__init__( + friendly_name, + "conditions", + value=lambda wu: wu.data['current_observation'][field], + icon=icon, + unit_of_measurement=unit_of_measurement, + entity_picture=lambda wu: wu.data['current_observation'][ + 'icon_url'] if icon is None else None, + device_state_attributes={ + 'date': lambda wu: wu.data['current_observation'][ + 'observation_time'] + } + ) + + +class WUDailyTextForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for daily text forecasts.""" + + def __init__(self, period, field, unit_of_measurement=None): + """Constructor. + + Args: + period (int): forecast period number + field (string): field name to use as value + unit_of_measurement(string): unit of measurement + """ + super().__init__( + friendly_name=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period]['title'], + feature='forecast', + value=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period][field], + entity_picture=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period]['icon_url'], + unit_of_measurement=unit_of_measurement, + device_state_attributes={ + 'date': lambda wu: wu.data['forecast']['txt_forecast']['date'] + } + ) + + +class WUDailySimpleForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for daily simpleforecasts.""" + + def __init__(self, friendly_name, period, field, wu_unit=None, + ha_unit=None, icon=None): + """Constructor. + + Args: + period (int): forecast period number + field (string): field name to use as value + wu_unit (string): "fahrenheit", "celsius", "degrees" etc. + see the example json at: + https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 + ha_unit (string): coresponding unit in home assistant + title (string): friendly_name of the sensor + """ + super().__init__( + friendly_name=friendly_name, + feature='forecast', + value=(lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period][field][wu_unit]) + if wu_unit else + (lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period][field]), + unit_of_measurement=ha_unit, + entity_picture=lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period]['icon_url'] if not icon else None, + icon=icon, + device_state_attributes={ + 'date': lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period]['date']['pretty'] + } + ) + + +class WUHourlyForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for hourly text forecasts.""" + + def __init__(self, period, field): + """Constructor. + + Args: + period (int): forecast period number + field (int): field name to use as value + """ + super().__init__( + friendly_name=lambda wu: "{} {}".format( + wu.data['hourly_forecast'][period]['FCTTIME'][ + 'weekday_name_abbrev'], + wu.data['hourly_forecast'][period]['FCTTIME'][ + 'civil']), + feature='hourly', + value=lambda wu: wu.data['hourly_forecast'][period][ + field], + entity_picture=lambda wu: wu.data['hourly_forecast'][ + period]["icon_url"], + device_state_attributes={ + 'temp_c': lambda wu: wu.data['hourly_forecast'][ + period]['temp']['metric'], + 'temp_f': lambda wu: wu.data['hourly_forecast'][ + period]['temp']['english'], + 'dewpoint_c': lambda wu: wu.data['hourly_forecast'][ + period]['dewpoint']['metric'], + 'dewpoint_f': lambda wu: wu.data['hourly_forecast'][ + period]['dewpoint']['english'], + 'precip_prop': lambda wu: wu.data['hourly_forecast'][ + period]['pop'], + 'sky': lambda wu: wu.data['hourly_forecast'][ + period]['sky'], + 'precip_mm': lambda wu: wu.data['hourly_forecast'][ + period]['qpf']['metric'], + 'precip_in': lambda wu: wu.data['hourly_forecast'][ + period]['qpf']['english'], + 'humidity': lambda wu: wu.data['hourly_forecast'][ + period]['humidity'], + 'wind_kph': lambda wu: wu.data['hourly_forecast'][ + period]['wspd']['metric'], + 'wind_mph': lambda wu: wu.data['hourly_forecast'][ + period]['wspd']['english'], + 'pressure_mb': lambda wu: wu.data['hourly_forecast'][ + period]['mslp']['metric'], + 'pressure_inHg': lambda wu: wu.data['hourly_forecast'][ + period]['mslp']['english'], + 'date': lambda wu: wu.data['hourly_forecast'][ + period]['FCTTIME']['pretty'], + }, + ) + + +class WUAlmanacSensorConfig(WUSensorConfig): + """Helper for defining field configurations for almanac sensors.""" + + def __init__(self, friendly_name, field, value_type, wu_unit, + unit_of_measurement, icon): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + field (string): value name returned in 'almanac' dict + as returned by the WU API + value_type (string): "record" or "normal" + wu_unit (string): unit name in WU API + icon (string): icon name or URL + unit_of_measurement (string): unit of meassurement + """ + super().__init__( + friendly_name=friendly_name, + feature="almanac", + value=lambda wu: wu.data['almanac'][field][value_type][wu_unit], + unit_of_measurement=unit_of_measurement, + icon=icon + ) + + +class WUAlertsSensorConfig(WUSensorConfig): + """Helper for defining field configuration for alerts.""" + + def __init__(self, friendly_name): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + """ + super().__init__( + friendly_name=friendly_name, + feature="alerts", + value=lambda wu: len(wu.data['alerts']), + icon=lambda wu: "mdi:alert-circle-outline" + if len(wu.data['alerts']) > 0 + else "mdi:check-circle-outline", + device_state_attributes=self._get_attributes + ) + + @staticmethod + def _get_attributes(rest): + + attrs = {} + + if 'alerts' not in rest.data: + return attrs + + alerts = rest.data['alerts'] + multiple_alerts = len(alerts) > 1 + for data in alerts: + for alert in ALERTS_ATTRS: + if data[alert]: + if multiple_alerts: + dkey = alert.capitalize() + '_' + data['type'] + else: + dkey = alert.capitalize() + attrs[dkey] = data[alert] + return attrs + + +# Declaration of supported WU sensors +# (see above helper classes for argument explanation) -# Sensor types are defined like: Name, units SENSOR_TYPES = { - 'alerts': ['Alerts', None], - 'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS], - 'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], - 'dewpoint_string': ['Dewpoint Summary', None], - 'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS], - 'feelslike_f': ['Feels Like (°F)', TEMP_FAHRENHEIT], - 'feelslike_string': ['Feels Like', None], - 'heat_index_c': ['Dewpoint (°C)', TEMP_CELSIUS], - 'heat_index_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], - 'heat_index_string': ['Heat Index Summary', None], - 'elevation': ['Elevation', 'ft'], - 'location': ['Location', None], - 'observation_time': ['Observation Time', None], - 'precip_1hr_in': ['Precipation 1hr', 'in'], - 'precip_1hr_metric': ['Precipation 1hr', 'mm'], - 'precip_1hr_string': ['Precipation 1hr', None], - 'precip_today_in': ['Precipation Today', 'in'], - 'precip_today_metric': ['Precipitation Today', 'mm'], - 'precip_today_string': ['Precipitation today', None], - 'pressure_in': ['Pressure', 'in'], - 'pressure_mb': ['Pressure', 'mb'], - 'pressure_trend': ['Pressure Trend', None], - 'relative_humidity': ['Relative Humidity', '%'], - 'station_id': ['Station ID', None], - 'solarradiation': ['Solar Radiation', None], - 'temperature_string': ['Temperature Summary', None], - 'temp_c': ['Temperature (°C)', TEMP_CELSIUS], - 'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT], - 'UV': ['UV', None], - 'visibility_km': ['Visibility (km)', 'km'], - 'visibility_mi': ['Visibility (miles)', 'mi'], - 'weather': ['Weather Summary', None], - 'wind_degrees': ['Wind Degrees', None], - 'wind_dir': ['Wind Direction', None], - 'wind_gust_kph': ['Wind Gust', 'kph'], - 'wind_gust_mph': ['Wind Gust', 'mph'], - 'wind_kph': ['Wind Speed', 'kph'], - 'wind_mph': ['Wind Speed', 'mph'], - 'wind_string': ['Wind Summary', None], + 'alerts': WUAlertsSensorConfig('Alerts'), + 'dewpoint_c': WUCurrentConditionsSensorConfig( + 'Dewpoint', 'dewpoint_c', 'mdi:water', TEMP_CELSIUS), + 'dewpoint_f': WUCurrentConditionsSensorConfig( + 'Dewpoint', 'dewpoint_f', 'mdi:water', TEMP_FAHRENHEIT), + 'dewpoint_string': WUCurrentConditionsSensorConfig( + 'Dewpoint Summary', 'dewpoint_string', 'mdi:water'), + 'feelslike_c': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_c', 'mdi:thermometer', TEMP_CELSIUS), + 'feelslike_f': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_f', 'mdi:thermometer', TEMP_FAHRENHEIT), + 'feelslike_string': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_string', "mdi:thermometer"), + 'heat_index_c': WUCurrentConditionsSensorConfig( + 'Heat index', 'heat_index_c', "mdi:thermometer", TEMP_CELSIUS), + 'heat_index_f': WUCurrentConditionsSensorConfig( + 'Heat index', 'heat_index_f', "mdi:thermometer", TEMP_FAHRENHEIT), + 'heat_index_string': WUCurrentConditionsSensorConfig( + 'Heat Index Summary', 'heat_index_string', "mdi:thermometer"), + 'elevation': WUSensorConfig( + 'Elevation', + 'conditions', + value=lambda wu: wu.data['current_observation'][ + 'observation_location']['elevation'].split()[0], + unit_of_measurement=LENGTH_FEET, + icon="mdi:elevation-rise"), + 'location': WUSensorConfig( + 'Location', + 'conditions', + value=lambda wu: wu.data['current_observation'][ + 'display_location']['full'], + icon="mdi:map-marker"), + 'observation_time': WUCurrentConditionsSensorConfig( + 'Observation Time', 'observation_time', "mdi:clock"), + 'precip_1hr_in': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_in', "mdi:umbrella", LENGTH_INCHES), + 'precip_1hr_metric': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_metric', "mdi:umbrella", 'mm'), + 'precip_1hr_string': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_string', "mdi:umbrella"), + 'precip_today_in': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_in', "mdi:umbrella", + LENGTH_INCHES), + 'precip_today_metric': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_metric', "mdi:umbrella", 'mm'), + 'precip_today_string': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_string', "mdi:umbrella"), + 'pressure_in': WUCurrentConditionsSensorConfig( + 'Pressure', 'pressure_in', "mdi:gauge", 'inHg'), + 'pressure_mb': WUCurrentConditionsSensorConfig( + 'Pressure', 'pressure_mb', "mdi:gauge", 'mb'), + 'pressure_trend': WUCurrentConditionsSensorConfig( + 'Pressure Trend', 'pressure_trend', "mdi:gauge"), + 'relative_humidity': WUSensorConfig( + 'Relative Humidity', + 'conditions', + value=lambda wu: int(wu.data['current_observation'][ + 'relative_humidity'][:-1]), + unit_of_measurement='%', + icon="mdi:water-percent"), + 'station_id': WUCurrentConditionsSensorConfig( + 'Station ID', 'station_id', "mdi:home"), + 'solarradiation': WUCurrentConditionsSensorConfig( + 'Solar Radiation', 'solarradiation', "mdi:weather-sunny", "w/m2"), + 'temperature_string': WUCurrentConditionsSensorConfig( + 'Temperature Summary', 'temperature_string', "mdi:thermometer"), + 'temp_c': WUCurrentConditionsSensorConfig( + 'Temperature', 'temp_c', "mdi:thermometer", TEMP_CELSIUS), + 'temp_f': WUCurrentConditionsSensorConfig( + 'Temperature', 'temp_f', "mdi:thermometer", TEMP_FAHRENHEIT), + 'UV': WUCurrentConditionsSensorConfig( + 'UV', 'UV', "mdi:sunglasses"), + 'visibility_km': WUCurrentConditionsSensorConfig( + 'Visibility (km)', 'visibility_km', "mdi:eye", LENGTH_KILOMETERS), + 'visibility_mi': WUCurrentConditionsSensorConfig( + 'Visibility (miles)', 'visibility_mi', "mdi:eye", LENGTH_MILES), + 'weather': WUCurrentConditionsSensorConfig( + 'Weather Summary', 'weather', None), + 'wind_degrees': WUCurrentConditionsSensorConfig( + 'Wind Degrees', 'wind_degrees', "mdi:weather-windy", "°"), + 'wind_dir': WUCurrentConditionsSensorConfig( + 'Wind Direction', 'wind_dir', "mdi:weather-windy"), + 'wind_gust_kph': WUCurrentConditionsSensorConfig( + 'Wind Gust', 'wind_gust_kph', "mdi:weather-windy", 'kph'), + 'wind_gust_mph': WUCurrentConditionsSensorConfig( + 'Wind Gust', 'wind_gust_mph', "mdi:weather-windy", 'mph'), + 'wind_kph': WUCurrentConditionsSensorConfig( + 'Wind Speed', 'wind_kph', "mdi:weather-windy", 'kph'), + 'wind_mph': WUCurrentConditionsSensorConfig( + 'Wind Speed', 'wind_mph', "mdi:weather-windy", 'mph'), + 'wind_string': WUCurrentConditionsSensorConfig( + 'Wind Summary', 'wind_string', "mdi:weather-windy"), + 'temp_high_record_c': WUAlmanacSensorConfig( + lambda wu: 'High Temperature Record ({})'.format( + wu.data['almanac']['temp_high']['recordyear']), + 'temp_high', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_high_record_f': WUAlmanacSensorConfig( + lambda wu: 'High Temperature Record ({})'.format( + wu.data['almanac']['temp_high']['recordyear']), + 'temp_high', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_low_record_c': WUAlmanacSensorConfig( + lambda wu: 'Low Temperature Record ({})'.format( + wu.data['almanac']['temp_low']['recordyear']), + 'temp_low', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_low_record_f': WUAlmanacSensorConfig( + lambda wu: 'Low Temperature Record ({})'.format( + wu.data['almanac']['temp_low']['recordyear']), + 'temp_low', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_low_avg_c': WUAlmanacSensorConfig( + 'Historic Average of Low Temperatures for Today', + 'temp_low', 'normal', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_low_avg_f': WUAlmanacSensorConfig( + 'Historic Average of Low Temperatures for Today', + 'temp_low', 'normal', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_high_avg_c': WUAlmanacSensorConfig( + 'Historic Average of High Temperatures for Today', + 'temp_high', 'normal', 'C', TEMP_CELSIUS, "mdi:thermometer"), + 'temp_high_avg_f': WUAlmanacSensorConfig( + 'Historic Average of High Temperatures for Today', + 'temp_high', 'normal', 'F', TEMP_FAHRENHEIT, "mdi:thermometer"), + 'weather_1d': WUDailyTextForecastSensorConfig(0, "fcttext"), + 'weather_1d_metric': WUDailyTextForecastSensorConfig(0, "fcttext_metric"), + 'weather_1n': WUDailyTextForecastSensorConfig(1, "fcttext"), + 'weather_1n_metric': WUDailyTextForecastSensorConfig(1, "fcttext_metric"), + 'weather_2d': WUDailyTextForecastSensorConfig(2, "fcttext"), + 'weather_2d_metric': WUDailyTextForecastSensorConfig(2, "fcttext_metric"), + 'weather_2n': WUDailyTextForecastSensorConfig(3, "fcttext"), + 'weather_2n_metric': WUDailyTextForecastSensorConfig(3, "fcttext_metric"), + 'weather_3d': WUDailyTextForecastSensorConfig(4, "fcttext"), + 'weather_3d_metric': WUDailyTextForecastSensorConfig(4, "fcttext_metric"), + 'weather_3n': WUDailyTextForecastSensorConfig(5, "fcttext"), + 'weather_3n_metric': WUDailyTextForecastSensorConfig(5, "fcttext_metric"), + 'weather_4d': WUDailyTextForecastSensorConfig(6, "fcttext"), + 'weather_4d_metric': WUDailyTextForecastSensorConfig(6, "fcttext_metric"), + 'weather_4n': WUDailyTextForecastSensorConfig(7, "fcttext"), + 'weather_4n_metric': WUDailyTextForecastSensorConfig(7, "fcttext_metric"), + 'weather_1h': WUHourlyForecastSensorConfig(0, "condition"), + 'weather_2h': WUHourlyForecastSensorConfig(1, "condition"), + 'weather_3h': WUHourlyForecastSensorConfig(2, "condition"), + 'weather_4h': WUHourlyForecastSensorConfig(3, "condition"), + 'weather_5h': WUHourlyForecastSensorConfig(4, "condition"), + 'weather_6h': WUHourlyForecastSensorConfig(5, "condition"), + 'weather_7h': WUHourlyForecastSensorConfig(6, "condition"), + 'weather_8h': WUHourlyForecastSensorConfig(7, "condition"), + 'weather_9h': WUHourlyForecastSensorConfig(8, "condition"), + 'weather_10h': WUHourlyForecastSensorConfig(9, "condition"), + 'weather_11h': WUHourlyForecastSensorConfig(10, "condition"), + 'weather_12h': WUHourlyForecastSensorConfig(11, "condition"), + 'weather_13h': WUHourlyForecastSensorConfig(12, "condition"), + 'weather_14h': WUHourlyForecastSensorConfig(13, "condition"), + 'weather_15h': WUHourlyForecastSensorConfig(14, "condition"), + 'weather_16h': WUHourlyForecastSensorConfig(15, "condition"), + 'weather_17h': WUHourlyForecastSensorConfig(16, "condition"), + 'weather_18h': WUHourlyForecastSensorConfig(17, "condition"), + 'weather_19h': WUHourlyForecastSensorConfig(18, "condition"), + 'weather_20h': WUHourlyForecastSensorConfig(19, "condition"), + 'weather_21h': WUHourlyForecastSensorConfig(20, "condition"), + 'weather_22h': WUHourlyForecastSensorConfig(21, "condition"), + 'weather_23h': WUHourlyForecastSensorConfig(22, "condition"), + 'weather_24h': WUHourlyForecastSensorConfig(23, "condition"), + 'weather_25h': WUHourlyForecastSensorConfig(24, "condition"), + 'weather_26h': WUHourlyForecastSensorConfig(25, "condition"), + 'weather_27h': WUHourlyForecastSensorConfig(26, "condition"), + 'weather_28h': WUHourlyForecastSensorConfig(27, "condition"), + 'weather_29h': WUHourlyForecastSensorConfig(28, "condition"), + 'weather_30h': WUHourlyForecastSensorConfig(29, "condition"), + 'weather_31h': WUHourlyForecastSensorConfig(30, "condition"), + 'weather_32h': WUHourlyForecastSensorConfig(31, "condition"), + 'weather_33h': WUHourlyForecastSensorConfig(32, "condition"), + 'weather_34h': WUHourlyForecastSensorConfig(33, "condition"), + 'weather_35h': WUHourlyForecastSensorConfig(34, "condition"), + 'weather_36h': WUHourlyForecastSensorConfig(35, "condition"), + 'temp_high_1d_c': WUDailySimpleForecastSensorConfig( + "High Temperature Today", 0, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_2d_c': WUDailySimpleForecastSensorConfig( + "High Temperature Tomorrow", 1, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_3d_c': WUDailySimpleForecastSensorConfig( + "High Temperature in 3 Days", 2, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_4d_c': WUDailySimpleForecastSensorConfig( + "High Temperature in 4 Days", 3, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_1d_f': WUDailySimpleForecastSensorConfig( + "High Temperature Today", 0, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_2d_f': WUDailySimpleForecastSensorConfig( + "High Temperature Tomorrow", 1, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_3d_f': WUDailySimpleForecastSensorConfig( + "High Temperature in 3 Days", 2, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_4d_f': WUDailySimpleForecastSensorConfig( + "High Temperature in 4 Days", 3, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_1d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature Today", 0, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_2d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature Tomorrow", 1, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_3d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature in 3 Days", 2, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_4d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature in 4 Days", 3, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_1d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature Today", 0, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_2d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature Tomorrow", 1, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_3d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature in 3 Days", 2, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_4d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature in 4 Days", 3, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'wind_gust_1d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind Today", 0, "maxwind", "kph", "kph", "mdi:weather-windy"), + 'wind_gust_2d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind Tomorrow", 1, "maxwind", "kph", "kph", "mdi:weather-windy"), + 'wind_gust_3d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 3 Days", 2, "maxwind", "kph", "kph", + "mdi:weather-windy"), + 'wind_gust_4d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 4 Days", 3, "maxwind", "kph", "kph", + "mdi:weather-windy"), + 'wind_gust_1d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind Today", 0, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_2d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind Tomorrow", 1, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_3d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 3 Days", 2, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_4d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 4 Days", 3, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_1d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Today", 0, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_2d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Tomorrow", 1, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_3d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 3 Days", 2, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_4d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 4 Days", 3, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_1d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Today", 0, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_2d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Tomorrow", 1, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_3d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 3 Days", 2, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_4d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 4 Days", 3, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'precip_1d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Today", 0, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_2d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_3d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_4d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_1d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Today", 0, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_2d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_3d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_4d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_1d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability Today", 0, "pop", None, "%", + "mdi:umbrella"), + 'precip_2d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability Tomorrow", 1, "pop", None, "%", + "mdi:umbrella"), + 'precip_3d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability in 3 Days", 2, "pop", None, "%", + "mdi:umbrella"), + 'precip_4d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability in 4 Days", 3, "pop", None, "%", + "mdi:umbrella"), } # Alert Attributes @@ -105,9 +617,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): - vol.All(vol.In(LANG_CODES)), + vol.All(vol.In(LANG_CODES)), vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -138,6 +650,20 @@ class WUndergroundSensor(Entity): """Initialize the sensor.""" self.rest = rest self._condition = condition + self.rest.request_feature(SENSOR_TYPES[condition].feature) + + def _cfg_expand(self, what, default=None): + cfg = SENSOR_TYPES[self._condition] + val = getattr(cfg, what) + try: + val = val(self.rest) + except (KeyError, IndexError) as err: + _LOGGER.error("Failed to parse response from WU API: %s", err) + val = default + except TypeError: + pass # val was not callable - keep original value + + return val @property def name(self): @@ -147,69 +673,42 @@ class WUndergroundSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data: - - if self._condition == 'elevation' and self._condition in \ - self.rest.data['observation_location']: - return self.rest.data['observation_location'][self._condition]\ - .split()[0] - - if self._condition == 'location' and \ - 'full' in self.rest.data['display_location']: - return self.rest.data['display_location']['full'] - - if self._condition in self.rest.data: - if self._condition == 'relative_humidity': - return int(self.rest.data[self._condition][:-1]) - else: - return self.rest.data[self._condition] - - if self._condition == 'alerts': - if self.rest.alerts: - return len(self.rest.alerts) - else: - return 0 - return STATE_UNKNOWN + return self._cfg_expand("value", STATE_UNKNOWN) @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} + attrs = self._cfg_expand("device_state_attributes", {}) + for (attr, callback) in attrs.items(): + try: + attrs[attr] = callback(self.rest) + except TypeError: + attrs[attr] = callback attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - - if not self.rest.alerts or self._condition != 'alerts': - return attrs - - multiple_alerts = len(self.rest.alerts) > 1 - for data in self.rest.alerts: - for alert in ALERTS_ATTRS: - if data[alert]: - if multiple_alerts: - dkey = alert.capitalize() + '_' + data['type'] - else: - dkey = alert.capitalize() - attrs[dkey] = data[alert] + attrs[ATTR_FRIENDLY_NAME] = self._cfg_expand("friendly_name") return attrs + @property + def icon(self): + """Return icon.""" + return self._cfg_expand("icon", super().icon) + @property def entity_picture(self): """Return the entity picture.""" - if self.rest.data and self._condition == 'weather': - url = self.rest.data['icon_url'] + url = self._cfg_expand("entity_picture") + if url is not None: return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) @property def unit_of_measurement(self): """Return the units of measurement.""" - return SENSOR_TYPES[self._condition][1] + return self._cfg_expand("unit_of_measurement") def update(self): """Update current conditions.""" - if self._condition == 'alerts': - self.rest.update_alerts() - else: - self.rest.update() + self.rest.update() class WUndergroundData(object): @@ -223,11 +722,16 @@ class WUndergroundData(object): self._lang = 'lang:{}'.format(lang) self._latitude = hass.config.latitude self._longitude = hass.config.longitude + self._features = set() self.data = None - self.alerts = None + + def request_feature(self, feature): + """Register feature to be fetched from WU API.""" + self._features.add(feature) def _build_url(self, baseurl=_RESOURCE): - url = baseurl.format(self._api_key, self._lang) + url = baseurl.format( + self._api_key, "/".join(self._features), self._lang) if self._pws_id: url = url + 'pws:{}'.format(self._pws_id) else: @@ -235,7 +739,7 @@ class WUndergroundData(object): return url + '.json' - @Throttle(MIN_TIME_BETWEEN_UPDATES_OBSERVATION) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from WUnderground.""" try: @@ -244,21 +748,7 @@ class WUndergroundData(object): raise ValueError(result['response']["error"] ["description"]) else: - self.data = result["current_observation"] + self.data = result except ValueError as err: _LOGGER.error("Check WUnderground API %s", err.args) self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES_ALERTS) - def update_alerts(self): - """Get the latest alerts data from WUnderground.""" - try: - result = requests.get(self._build_url(_ALERTS), timeout=10).json() - if "error" in result['response']: - raise ValueError(result['response']["error"] - ["description"]) - else: - self.alerts = result["alerts"] - except ValueError as err: - _LOGGER.error("Check WUnderground API %s", err.args) - self.alerts = None diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 286f9d959e2..1a3c0304b00 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -2,7 +2,7 @@ import unittest from homeassistant.components.sensor import wunderground -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES from tests.common import get_test_home_assistant @@ -19,7 +19,8 @@ VALID_CONFIG = { 'platform': 'wunderground', 'api_key': 'foo', 'monitored_conditions': [ - 'weather', 'feelslike_c', 'alerts', 'elevation', 'location' + 'weather', 'feelslike_c', 'alerts', 'elevation', 'location', + 'weather_1d_metric', 'precip_1d_in' ] } @@ -37,6 +38,8 @@ FEELS_LIKE = '40' WEATHER = 'Clear' HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif' ALERT_MESSAGE = 'This is a test alert message' +FORECAST_TEXT = 'Mostly Cloudy. Fog overnight.' +PRECIP_IN = 0.03 def mocked_requests_get(*args, **kwargs): @@ -60,7 +63,9 @@ def mocked_requests_get(*args, **kwargs): "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", "features": { - "conditions": 1 + "conditions": 1, + "alerts": 1, + "forecast": 1, } }, "current_observation": { "image": { @@ -90,7 +95,58 @@ def mocked_requests_get(*args, **kwargs): "message": ALERT_MESSAGE, }, - ], + ], "forecast": { + "txt_forecast": { + "date": "22:35 CEST", + "forecastday": [ + { + "period": 0, + "icon_url": + "http://icons.wxug.com/i/c/k/clear.gif", + "title": "Tuesday", + "fcttext": FORECAST_TEXT, + "fcttext_metric": FORECAST_TEXT, + "pop": "0" + }, + ], + }, "simpleforecast": { + "forecastday": [ + { + "date": { + "pretty": "19:00 CEST 4. Duben 2017", + }, + "period": 1, + "high": { + "fahrenheit": "56", + "celsius": "13", + }, + "low": { + "fahrenheit": "43", + "celsius": "6", + }, + "conditions": "Možnost deště", + "icon_url": + "http://icons.wxug.com/i/c/k/chancerain.gif", + "qpf_allday": { + "in": PRECIP_IN, + "mm": 1, + }, + "maxwind": { + "mph": 0, + "kph": 0, + "dir": "", + "degrees": 0, + }, + "avewind": { + "mph": 0, + "kph": 0, + "dir": "severní", + "degrees": 0 + } + }, + ], + }, + }, }, 200) else: return MockResponse({ @@ -168,7 +224,13 @@ class TestWundergroundSetup(unittest.TestCase): self.assertEqual('Holly Springs, NC', device.state) elif device.name == 'PWS_elevation': self.assertEqual('413', device.state) - else: + elif device.name == 'PWS_feelslike_c': self.assertIsNone(device.entity_picture) self.assertEqual(FEELS_LIKE, device.state) self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement) + elif device.name == 'PWS_weather_1d_metric': + self.assertEqual(FORECAST_TEXT, device.state) + else: + self.assertEqual(device.name, 'PWS_precip_1d_in') + self.assertEqual(PRECIP_IN, device.state) + self.assertEqual(LENGTH_INCHES, device.unit_of_measurement) From 2ab45441a8ebff8fd55860b5d61ac25dedf288ce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 May 2017 04:10:17 +0200 Subject: [PATCH 17/21] Upgrade pymysensors to 0.10.0 (#7469) --- homeassistant/components/mysensors.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 984ff8a4606..ef863bfb34f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -21,9 +21,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery from homeassistant.loader import get_component -REQUIREMENTS = [ - 'https://github.com/theolind/pymysensors/archive/' - 'c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1'] +REQUIREMENTS = ['pymysensors==0.10.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4f0edd805e1..65ad330b0e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,9 +318,6 @@ https://github.com/tfriedel/python-lightify/archive/1bb1db0e7bd5b14304d7bb267e23 # homeassistant.components.lutron https://github.com/thecynic/pylutron/archive/v0.1.0.zip#pylutron==0.1.0 -# homeassistant.components.mysensors -https://github.com/theolind/pymysensors/archive/c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1 - # homeassistant.components.sensor.modem_callerid https://github.com/vroomfonde1/basicmodem/archive/0.7.zip#basicmodem==0.7 @@ -593,6 +590,9 @@ pymailgunner==1.4 # homeassistant.components.mochad pymochad==0.1.1 +# homeassistant.components.mysensors +pymysensors==0.10.0 + # homeassistant.components.device_tracker.netgear pynetgear==0.3.3 From f87b9b7b857550982d37baff999eea64a262b261 Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Sun, 7 May 2017 15:15:18 +0200 Subject: [PATCH 18/21] Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation (#7476) * Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation small_float only allows values from 0 to 1 so we should use float instead * Do not use vol.All for a single validation --- homeassistant/components/plant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 2215d7c2f30..2070c22fb97 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -58,8 +58,8 @@ SCHEMA_SENSORS = vol.Schema({ PLANT_SCHEMA = vol.Schema({ vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), vol.Optional(CONF_MIN_BATTERY_LEVEL): cv.positive_int, - vol.Optional(CONF_MIN_TEMPERATURE): cv.small_float, - vol.Optional(CONF_MAX_TEMPERATURE): cv.small_float, + vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float), vol.Optional(CONF_MIN_MOISTURE): cv.positive_int, vol.Optional(CONF_MAX_MOISTURE): cv.positive_int, vol.Optional(CONF_MIN_CONDUCTIVITY): cv.positive_int, From 4165629f975322ce1b96491035d7892a0e172c2f Mon Sep 17 00:00:00 2001 From: Caleb Date: Sun, 7 May 2017 00:39:21 -0500 Subject: [PATCH 19/21] Update to pyunifi 2.12 (#7468) * Update to pyunifi 2.12 * Update requirements_all.txt --- homeassistant/components/device_tracker/unifi.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 42b5070b046..b0409e99883 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_VERIFY_SSL -REQUIREMENTS = ['pyunifi==2.0'] +REQUIREMENTS = ['pyunifi==2.12'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' diff --git a/requirements_all.txt b/requirements_all.txt index 65ad330b0e2..4f0ba65105b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ pytrackr==0.0.5 pytradfri==1.1 # homeassistant.components.device_tracker.unifi -pyunifi==2.0 +pyunifi==2.12 # homeassistant.components.keyboard # pyuserinput==0.1.11 From ab9c394e93e467dcbf1d87dff19a7e6f44310f52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 May 2017 15:09:53 -0700 Subject: [PATCH 20/21] Version bump to 0.44.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d4014a7f161..3946e4d20a7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 44 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 12293d6600feb2598cd039aa08739e8ffb59b896 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 May 2017 22:07:52 -0700 Subject: [PATCH 21/21] 0.44.2 (#7488) * Version bump to 0.44.2 * Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../www_static/home-assistant-polymer | 2 +- .../www_static/panels/ha-panel-hassio.html | 4 ++-- .../www_static/panels/ha-panel-hassio.html.gz | Bin 7451 -> 7383 bytes .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2513 -> 2513 bytes homeassistant/const.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 943074beb40..0d649344862 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -12,7 +12,7 @@ FINGERPRINTS = { "panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", "panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", "panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", - "panels/ha-panel-hassio.html": "333f86e5f516b31e52365e412deb7fdc", + "panels/ha-panel-hassio.html": "23d175b6744c20e2fdf475b6efdaa1d3", "panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index f020e60b67e..9e7dc4a921f 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit f020e60b67ec38e3ede72b1ebc86d4e055565cd7 +Subproject commit 9e7dc4a921f86e60cc1f14afe254e5310b63e854 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html index c82bfcb54e8..80d1686acf0 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html @@ -12,6 +12,6 @@ }, computeInstallStatus(addon) { - return addon.installed || 'Not installed'; + return (addon && addon.installed) || 'Not installed'; }, -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index c7689872442293fb47f9500bae9c7482f0077000..5ed1205a99958263848ffedc2f48e134b50c3172 100644 GIT binary patch literal 7383 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 J&=&H9006*(RyzOy literal 7451 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 delta 1425 zcmV;C1#bG$6VVd~ABzYGYyk|h2ZjTGQL12^YMK=(@;Wof@PegjOcRnJ54VCrs1#XR zP@W5u#kq)csYspzRf*elFL-M;8r@CWMmDyYyzGC*KQ@()tToO?nsC&LM&cd}A*tXj zO%fr5CNj?Gw?zF?&KGtgE0@g3qA-X78b?xUkkJXjI7*U)Bo3_?nK#-lg9`Wug}5C03}uggrf) zi8|F_rT4rbEax@GD7HE;M2iwF*2rH47Fw)rvv-r<*LL|~@;($=|C1yIf`5Q$GYXym z)^1`LY@T1kXSl>cz=3G1{>@QpYD9uHA^_z2XFS)w^X5Pu~t9#1l0!&{bTGy zX#*m&?O?+{5=0#~ebz^$g7Z!L=9`)4I(Ji6sQJ`~Ho`uwy*;~a{66UW z|FgP2Hf6n4HH^`;M=`Mw-J-`}2>lOxvG0X=!suQ%TWWPZ8o#2p!+)Oi$N&eOojC); z8h;3e`h(}fK5bT`W`_j!#i32e;U{E^{yVW}e5l^v2f?(Lhq3d=RU@Vt zw-`GWR?~gUAi5EyeN)@A^7|ch5$3Gg-zC5Er>WyJ+}W7lMk%WIe`s5A8)0j)^dF%a zU5jcBol71E0M>mJpnsbY1{Xm+oCAO_;oocL`#QWs&@}G16jgWSb!hn*PZ#sD_U?B~ z9nLo5gq?%@=2Ku7ZFAG-sGr)lLDV)HMy7Fxllsyd4Z=W=1P+HhHo7V8Vl<>CcZAtw zsh#43JnLfh98eIg)^0bpC-Oad^TdI6&jaY_?o+vXi#{Ca>wgx6X9(bqtJg@ZLk0}o z+#Ze3x@7u)Fy+jdzCOv1`fJX2NBkhzi5-rv_{|>yN>!!n1n^gs%Wpu{GvyE`w8Hz_ zPp`c`8Mgx~qTwXG0TE;SY%cX5*5DSt)(-?zJA3Sff%sF4rKp}AbqUB93~n3@-Et6~ z6A(Rn?cxLG!~+(|&>$ z`>HQXQJ2QaawmL@;^5DJ!iMZ%=Y31E%YE+Tewh9Cv)S2ggw|bY48f=0V(9(vgd7Us zWBGIFOZ4$NbUy58Hyx71CPBPjz^GTLAJTzc2s^qF-G2x^uln9;RCXg|c!wK{yn(8! zx8Ot$a7}bmxo~T1|NCdny3+N(zPBTXH$3R*6Vi8`H*%?Tu;6O+-Sh7c@y1NDxzgE9 z_sl;}{nff#oX8N<;eH6+hlOl&^VGQ*3-=Sli5_^--2Aq8&E;dYE^XyHies3M@7nsi zG7s4f<$pD3Z*!&QZqYWkZtlljr9Ir91z!v7{Tnw-2<|%Tc&q1c1igNZGX&hm5AN?& z)nIZznjTO)G3ng1f%D|Mz^&TLxtsLWGxrBflpAbrF!Y-n@6@$t-?4>aid4a%i>LKp zwr%9JzPt>O)B?Ma8oLpv53_;G4yIA-&NcaBOEP#7^>cnO#T8pKT&TV6RR_Bw_xO!( fzn*0eoq^~bI^(Nuz^{8RV0Hg5e3-=mV-^4an5xvR diff --git a/homeassistant/const.py b/homeassistant/const.py index 3946e4d20a7..57d4da7bc19 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 44 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)