라즈베리파이로 지역난방 IOT 만들기 #3 개발

2018. 11. 15. 10:48


 개발


개발은 Python을 이용해서 개발하였다.

프로그램은 총 2개이다.

1. 보일러 제어 프로그램

- 하나는 온도센서로 부터 데이터를 읽어, 릴레이를 제어하고, DB에 온도와 control한 정보를 기록한다.


2. 웹서버

- DB에 저장된 정보를 읽어 온도 변화를 보여주고, 원격으로 재부팅과 종료를 수행한다.



위에 그림에서,

저 날은 안방만 보일러를 켰다.

오후 10시에 보일러를 켯고, 난방수가 나가는 온도가 약 30도 조금 넘었을 때

더 이상 온도가 안올라가서, 구동기를 OFF 한 경우이다.

구동기가 OFF가 되니, 뜨거운 난방수가 더이상 유입이 안되고,

시간이 지나니 점차 온도가 떨어지는 것을 볼 수 있다.


테스트를 통해 28도 이하로 내려가면,

다시 On 시키도록 해놨다.

28도 사실 좀 높은 온도이긴 한데,

실험을 해보니, 난방수가 유입되면서 

싱크대 밑에 공기가 따뜻해져서 어느정도 공기의 온도도 올라가는 것 같았다.

그리고, 나머지 방의 경우는 보일러를 키지도 않았는데,

안방의 보일러를 키니, 

같이 어느정도 올라가는 것을 볼  수 있다.

이것도 배관에 어느정도 온도가 전달되는 듯한 느낌을 받았다. 

혹은 공기 온도가 올라가니, 온도센서가 반응한 것일 수도 있고..


어찌되었던, 원하는대로 온도 조절기를 제어할 수 있어서 좋았다.


 보일러 제어 프로그램 코드

 

# -*- coding: utf-8 -*-
import os # import os module
import glob # import glob module
import time # import time module
import MySQLdb
from datetime import datetime
import RPi.GPIO as GPIO

def read_temp_raw(device_file):
f = open(device_file, 'r')
lines = f.readlines() # read the device details
f.close()
return lines

def read_temp(device_file):
lines = read_temp_raw(device_file)
# while lines[0].strip()[-3:] != 'YES': # ignore first line
# print "retry!!!"
# time.sleep(0.2)
# lines = read_temp_raw(device_file)

equals_pos = lines[1].find('t=') # find temperature in the details
if equals_pos != -1:
temp_string = lines[1][equals_pos+2:]
temp_c = float(temp_string) / 1000.0 # convert to Celsius
# temp_f = temp_c * 9.0 / 5.0 + 32.0 # convert to Fahrenheit
return temp_c #, temp_f


def create_table(db):
cur = db.cursor()

sql = """CREATE TABLE IF NOT EXISTS temp_{} (
roomid varchar(20) NOT NULL,
sensorid varchar(40),
now datetime,
temp float
)
""".format(datetime.now().strftime('%Y%m%d'))
cur.execute(sql)

def create_control_table(db):
cur = db.cursor()

sql = """CREATE TABLE IF NOT EXISTS control_onoff (
roomid varchar(20) NOT NULL,
sensorid varchar(40),
now datetime,
on_off int
)
""".format(datetime.now().strftime('%Y%m%d'))
cur.execute(sql)

def insert_table(db, room, sensorid, temp, measure_time):
try:
cur = db.cursor()
sql = """INSERT INTO temp_{} VALUES('{}','{}','{}',{})
""".format(datetime.now().strftime('%Y%m%d'),
room,
sensorid,
measure_time,
temp)
# print sql
cur.execute(sql)
db.commit()
except Exception as e:
if e[0] == 1146:
create_table(db)


def insert_control_table(db, room, sensorid, onoff, measure_time):
try:
cur = db.cursor()
sql = """INSERT INTO control_onoff VALUES('{}','{}','{}',{})
""".format(room,
sensorid,
measure_time,
onoff)
# print sql
cur.execute(sql)
db.commit()
except Exception as e:
if e[0] == 1146:
create_control_table(db)


def get_control_states(room):
try:
cur = db.cursor()
sql = """select *
from control_onoff
where roomid = '{}'
order by now desc limit 1""".format(room)
cur.execute(sql)
rows = cur.fetchall()
return rows[0][3]
except Exception as e:
if e[0] == 1146:
create_control_table(db)
print e
return None


def set_boiler_control(room_id, is_relay_on):
gpio_map = dict()
gpio_map['room_sijoo'] = 17
gpio_map['room_sunny'] = 18
gpio_map['room_living'] = 19
if is_relay_on:
print "{} : relay On -> heat Off".format(room_id)
else:
print "{} : relay Off -> heat On".format(room_id)

GPIO.output(gpio_map[room_id], is_relay_on) #GPIO를 True로 바꾸어 보일러 해제


if __name__ == '__main__':
base_dir = '/sys/bus/w1/devices/'
room_map = dict()
room_map['28-01131b9b9966'] = 'room_sunny'
room_map['28-0213135108aa'] = 'room_sijoo'
room_map['28-0213135308aa'] = 'room_living'
condition_map = dict()
condition_map['room_sijoo'] = {'max' : 37, 'min' : 28}
condition_map['room_sunny'] = {'max' : 32, 'min' : 26}
condition_map['room_living'] = {'max' : 37, 'min' : 28}

time_check_map = dict()
time_check_map['room_sijoo'] = {'max' : 0, 'count' : 0}
time_check_map['room_sunny'] = {'max' : 0, 'count' : 0}
time_check_map['room_living'] = {'max' : 0, 'count' : 0}


sensor_list = glob.glob(base_dir + '28*') # point to the address
print sensor_list
device_folder = dict()

for sensor in sensor_list:
sensor_id = sensor.split('/')[-1]
if sensor_id in room_map:
print "{} : {}".format(sensor_id, room_map[sensor_id])
device_folder[sensor_id] = sensor + '/w1_slave'

# print device_folder
GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.OUT) #1
GPIO.setup(18,GPIO.OUT) #2
GPIO.setup(19,GPIO.OUT) #3

# DB 연결
while True:
try:
db = MySQLdb.connect(host="localhost", # your host, usually localhost
user="root", # your username
passwd="비번", # your password
db="디비명") # name of the data base
break
except:
pass


#마지막 상태값을 이용해서 GPIO 셋팅
for k, v in room_map.items():
ret = get_control_states(v)
print "{} : {}".format(v, ret)
if ret == 0:
set_boiler_control(v, True) #GPIO를 True로 바꾸어 보일러 해제
elif ret == 1:
set_boiler_control(v, False) #GPIO를 False로 바꾸어 보일러 켜기
elif ret == -1:
set_boiler_control(v, True) #GPIO를 True로 바꾸어 보일러 해제


#온도 가져오기
t = datetime.now()
is_control_mode = True
while True:
if (datetime.now()-t).seconds >= 10:
t = datetime.now()
measure_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
for id, df in device_folder.items():
temp = read_temp(df)
# print "[{}] {} : {}".format(measure_time, room_map[id], temp)
insert_table(db, room_map[id], id, temp, measure_time)

#특정온도보다 높다면
if temp > condition_map[room_map[id]]['max']:
#GPIO를 True로 바꾸어 보일러 해제
set_boiler_control(room_map[id], True)
last_states = get_control_states(room_map[id])
if last_states > 0:
# 컨트롤 DB에 끈 상태로 저장
insert_control_table(db,
room_map[id],
id, 0, measure_time)

# 특정온도보다 낮다면
if temp < condition_map[room_map[id]]['min']:
#보일러가 다시 켜질 수 있도록 On
set_boiler_control(room_map[id], False)
last_states = get_control_states(room_map[id])
if last_states <= 0:
# 컨트롤 DB에 켜진 상태로 저장
insert_control_table(db,
room_map[id],
id, 1, measure_time)
# 저장된 값보다 크면 증가중이므로 아무것도 안함
if temp > 30:
if temp > time_check_map[room_map[id]]['max']:
time_check_map[room_map[id]]['max'] = temp
time_check_map[room_map[id]]['count'] = 0
else: # 만약 현재 max값이 변화가 일정 시간 없다면 자동 해제 처리
time_check_map[room_map[id]]['count'] =
time_check_map[room_map[id]]['count'] + 1
if time_check_map[room_map[id]]['count'] > 5:
print "Time check : GPIO OFF [safety]!!"
#GPIO를 True로 바꾸어 보일러 해제
set_boiler_control(room_map[id], True)
last_states = get_control_states(room_map[id])
# 컨트롤 DB에 끈 상태로 저장
if last_states > 0:
insert_control_table(db,
room_map[id],
id, -1, measure_time)
#초기화
time_check_map[room_map[id]]['max'] = 0
time_check_map[room_map[id]]['count'] = 0
else:
time_check_map[room_map[id]]['max'] = 0
time_check_map[room_map[id]]['count'] = 0



db.close()



 웹서버 코드

 


import MySQLdb
from datetime import datetime

import time
from flask import Flask, render_template, Markup
import socketio
import os.path, time
import shutil
import json

sio = socketio.Server(logger=True, async_mode=None)
app = Flask(__name__, static_url_path = "", static_folder = "temp")
app.wsgi_app = socketio.Middleware(sio, app.wsgi_app)
app.config['SECRET_KEY'] = 'secret!'

@app.route('/')
def index():
data = get_recent_data_json()
marker = get_marker_data_json(eval(data)[0])
return render_template('index.html', data = Markup(data), marker_data = Markup(marker))

@sio.on('get_recent', namespace='/tauto')
def asm_get_recent(sid, msg):
data = get_recent_data_json()
marker = get_marker_data_json(eval(data)[0])
sio.emit('temp_data', {'data': data, 'marker' : marker}, room=sid, namespace='/tauto')

@sio.on('get_all', namespace='/tauto')
def asm_get_recent(sid, msg):
data = get_temp_data_json()
marker = get_marker_data_json(eval(data)[0])
sio.emit('temp_data', {'data': data, 'marker' : marker}, room=sid, namespace='/tauto')


@sio.on('connect', namespace='/tauto')
def asm_connect(sid, environ):
print("Connect {}".format(environ['REMOTE_ADDR']))

@sio.on('reboot', namespace='/tauto')
def asm_reboot(sid, msg):
os.system("sudo reboot")
print "reboot"
@sio.on('shutdown', namespace='/tauto')
def asm_shutdown(sid, msg):
os.system("sudo shutdown -h now")
print "shutdown"


def get_recent_data_json():
db = MySQLdb.connect(host="localhost", # your host, usually localhost
user="root", # your username
passwd="비번", # your password
db="디비명") # name of the data base

try:
cur = db.cursor()
sql = """
Select *
FROM
(
select now, max(sunny) as r1, max(sijoo) as r2, max(living) as r3
from
(
select now,
case when roomid = 'room_sunny' then temp else 0 end as sunny,
case when roomid = 'room_sijoo' then temp else 0 end as sijoo,
case when roomid = 'room_living' then temp else 0 end as living
from temp_{} order by now desc limit 3000
) as a
group by now
) as t
Where r1 * r2 * r3 > 0
order by now asc
""".format(datetime.now().strftime("%Y%m%d"))
cur.execute(sql)
rows = cur.fetchall()
except Exception as e:
return "db error : {}".format(e)
# print rows
db.close()

data = []
for r in rows:
d = dict()
d['date'] = "{}".format(r[0])
d['sunny'] = r[1]
d['sijoo'] = r[2]
d['living'] = r[3]
data.append(d)

return json.dumps(data)


def get_temp_data_json():
db = MySQLdb.connect(host="localhost", # your host, usually localhost
user="root", # your username
passwd="비번", # your password
db="디비") # name of the data base

try:
cur = db.cursor()
sql = """
select
b.now, r1, r2, r3
FROM
(
select
max(now) as nk,
hour(now) as a,
minute(now) as b from temp_{date} group by a, b
) as a left join
(
Select *
FROM
(
select now, max(sunny) as r1, max(sijoo) as r2, max(living) as r3
from
(
select now,
case when roomid = 'room_sunny' then temp else 0 end as sunny,
case when roomid = 'room_sijoo' then temp else 0 end as sijoo,
case when roomid = 'room_living' then temp else 0 end as living
from temp_{date} order by now desc
) as a
group by now
) as t
Where r1 * r2 * r3 > 0
order by now asc
) as b on a.nk = b.now where b.now is not null
""".format(date=datetime.now().strftime("%Y%m%d"))

cur.execute(sql)
rows = cur.fetchall()
except Exception as e:
return "db error : {}".format(e)
# print rows
db.close()
data = []
for r in rows:
d = dict()
d['date'] = "{}".format(r[0])
d['sunny'] = r[1]
d['sijoo'] = r[2]
d['living'] = r[3]
data.append(d)

return json.dumps(data)


def get_marker_data_json(start_data):
db = MySQLdb.connect(host="localhost", # your host, usually localhost
user="root", # your username
passwd="비번", # your password
db="디비명") # name of the data base

try:
cur = db.cursor()
sql = """
Select now, max(sunny), max(sijoo), max(living)
FROM
(
Select now,
case when roomid = 'room_sijoo' then on_off end as sijoo,
case when roomid = 'room_sunny' then on_off end as sunny,
case when roomid = 'room_living' then on_off end as living
FROM control_onoff
where now >= '{}'
) as t
group by now
order by now asc
""".format(start_data['date'])
sql = """
Select now, roomid, on_off
FROM control_onoff
where now >= '{}' order by now asc
""".format(start_data['date']) #datetime.now().strftime("%Y-%m-%d"))
print sql
cur.execute(sql)
rows = cur.fetchall()
except Exception as e:
return "db error : {}".format(e)
db.close()
data = []
for r in rows:
d = dict()
d['date'] = "{}".format(r[0])
d['room'] = r[1]
d['onoff'] = r[2]
data.append(d)

return json.dumps(data)



if __name__ == '__main__':
app.run(debug=True, threaded=True, host='0.0.0.0',port=80)

 index.html

 

<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<script data-require="d3@3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<!-- <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> -->
<!-- <script src="https://static.hogangnono.com/js/jquery-1.11.3.min.js"></script> -->
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>

<style>
body {
font: 10px sans-serif;
}

.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}

.x.axis path {
display: none;
stroke: #000;
}

.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.tooltip{
font: 12px sans-serif;
font-weight: bold;
background: #FFF;
}

.tick line{
stroke: #CCCCCC;
stroke-dasharray : 1;
}

.legend{
font: 13px sans-serif;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, '');
});

$(document).ready(function(){
$('.sidenav').sidenav();
});

function hideAddressBar()
{
if(!window.location.hash)
{
if(document.height < window.outerHeight)
{
document.body.style.height = (window.outerHeight + 50) + 'px';
}

setTimeout( function(){ window.scrollTo(0, 1); }, 50 );
}
}

var socket;
var featurefilename;
var raw_data = {{data}}
var raw_marker = {{marker_data}}
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
//날짜 변환
raw_data.forEach(function(d) {
d.date = parseDate(d.date);
});

$(document).ready(function(){
namespace = '/tauto';
socket = io.connect('http://' + document.domain + ':' + location.port + namespace);
console.log("connect")
$('#reboot').click(function() {
if (confirm("정말 재부팅하시겠습니까??") == true)
{ //확인
socket.emit('reboot', {});
}
return false;
});

$('#shutdown').click(function() {
if (confirm("정말 종료하시겠습니까??") == true)
{ //확인
socket.emit('shutdown', {});
}
return false;
});

$('#today').click(function() {
socket.emit('get_all', {});
return false;
});
$('#recent').click(function() {
socket.emit('get_recent', {});
return false;
});

socket.on('temp_data', function(msg) {
raw_data = JSON.parse(msg.data);
raw_marker = JSON.parse(msg.marker);
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
//날짜 변환
raw_data.forEach(function(d) {
d.date = parseDate(d.date);
});
draw(raw_data, raw_marker);
});

draw(raw_data, raw_marker);
window.addEventListener("load", function(){ if(!window.pageYOffset){ hideAddressBar(); } } );
window.addEventListener("orientationchange", hideAddressBar );
$( window ).resize(function() {
draw(raw_data, raw_marker);
});

$('.room').change(function(a){console.log($(this).val())})
});

document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('select');
var instances = M.FormSelect.init(elems, '');
});

// Or with jQuery

$(document).ready(function(){
$('select').formSelect();
});
var spinner = $( "#spinner" ).spinner();
</script>
</head>
<body style="width:100%;height:100%;margin:0px">
<div id="sijoo">
</div>
<div id='control_container' style="margin-left:10px;">
<a class="waves-effect waves-light btn" id='today'>Today</a>
<a class="waves-effect waves-light btn" id='recent'>Recent</a>
<a class="waves-effect waves-light btn" id='shutdown'>Shutdown</a>
<a class="waves-effect waves-light btn" id='reboot'>Reboot</a>

<ul id="slide-out" class="sidenav">
<li style="margin-left:10px;">
<div class="input-field col s12">
<select class="room">
<option value="" disabled selected>Choose your option</option>
<option value="sijoo">Sijoo</option>
<option value="sunny">Sunny</option>
<option value="living">Living</option>
</select>
</div>

</li>
<li>
<p>
<input id="spinner" name="value">
</p>
</li>
</ul>
<a class="waves-effect waves-light btn sidenav-trigger" data-target="slide-out">Control</a>
</div>
</body>
<script>
function draw(raw_data, raw_marker)
{
var data = raw_data;
var marker = raw_marker;

d3.select("#sijoo").select("svg").remove()
var margin = {
top: 15,
right: 90,
bottom: 30,
left: 50
},
bodyheight = $("body").height();
if(bodyheight > 900)
bodyheight = 500;
width = $("body").width() - margin.left - margin.right,
height = bodyheight-40 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
var color = d3.scale.ordinal()
.domain(["sijoo", "sunny", "living"])
.range(["#00A8BB", "#FFBC16", "#FF764C"]);
//x, y 축 범위 설정
x.domain(d3.extent(data, function(d) {
return d.date;
}));
y.domain([20,40
// d3.min(cities, function(c) {
// return d3.min(c.values, function(v) {
// return v.temperature;
// });
// }),
// d3.max(cities, function(c) {
// return d3.max(c.values, function(v) {
// return v.temperature;
// });
// })
]);
// y.domain([
// 20,
// 35
// ]);
var xAxis;
if((x.domain()[1]-x.domain()[0])/1000/60/60 < 6){
xAxis = d3.svg.axis().scale(x).orient("bottom").innerTickSize(-height).outerTickSize(0).tickPadding(10).ticks(d3.time.minute, 30);
}
else{
xAxis = d3.svg.axis().scale(x).orient("bottom").innerTickSize(-height).outerTickSize(0).tickPadding(10).ticks(d3.time.hour, 2);
}
var yAxis = d3.svg.axis().scale(y).orient("left").innerTickSize(-width).outerTickSize(0).tickPadding(10);
var line = d3.svg.line().interpolate("linear")
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.temperature);
});
//기본 컨버스 추가
var svg = d3.select("#sijoo").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("shape-rendering","geometricPrecision");
//키 셋팅
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "date";
}));
//데이터 변환
var cities = color.domain().map(function(name)
{
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
temperature: d[name]
};
})
};
});
var legend = svg.selectAll('g')
.data(cities)
.enter()
.append('g')
.attr('class', 'legend')
.style("cursor", "pointer")
.on('click', function(d) { // on mouse out hide line, circles and text
console.log(d)
s = d3.select("." + d.name).style("opacity");
if(s == 0)
d3.select("." + d.name).style("opacity", "1");
else
d3.select("." + d.name).style("opacity", "0");
});
legend.append('rect')
.attr('x', width)
.attr('y', function(d, i) {
return i * 20;
})
.attr('width', 10)
.attr('height', 10)
.style('fill', function(d) {
return color(d.name);
});
legend.append('text')
.attr('x', width + 14)
.attr('y', function(d, i) {
return (i * 20) + 9;
})
.text(function(d) {
return d.name;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Temperature(℃)");
var city = svg.selectAll(".city")
.data(cities)
.enter().append("g")
.attr("class", function(d){ return "city " + d.name});
city.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
});

var aa = svg.selectAll(".marker")
.data(marker)
.enter().append("g")
.attr("class", "marker")
.attr("transform", function(d, i){
for(k=0; k<cities.length; k++)
{
if( "room_" + cities[k].name == d.room)
break
}
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(cities[k].values, parseDate(d.date));
return "translate(" + x(parseDate(d.date)) + "," + y(cities[k].values[idx].temperature) + ")"
})
.append('circle')
.attr("r", 3)
.style("stroke", function(d) {
return color(d.name);
})
.style("fill", function(d){
if(d.onoff == 1)
return "#0F0"
else if(d.onoff == 0)
return "#F00"
else
return "#00F"
})
.style("stroke-width", "0px")
.style("opacity", "1")

var mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
mouseG.append("text")
.attr("class", "tooltip_date")
.text("test")
.style("opacity", "0");

var text_tooltip = mouseG.selectAll('.tooltip')
.data(cities)
.enter()
.append("text")
.attr("class", "tooltip")
.style("opacity", "0");

mouseG.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "#070")
.style("stroke-width", "2px")
.style("opacity", "0");
var lines = document.getElementsByClassName('line');
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(cities)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 4)
// .style("stroke",
.style("fill", function(d) {
return color(d.name);
})
.style("stroke-width", "1px")
.style("opacity", "0");

mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function() { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
d3.selectAll(".tooltip")
.style("opacity", "0");
d3.selectAll(".tooltip_date")
.style("opacity", "0");
})
.on('mouseover', function() { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
d3.selectAll(".tooltip")
.style("opacity", "1");
d3.selectAll(".tooltip_date")
.style("opacity", "1");
})
.on('mousemove', function() { // mouse moving over canvas
var mouse = d3.mouse(this);
d3.select(".mouse-line")
.attr("d", function() {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});

d3.selectAll(".tooltip_date").attr("transform", function(d, i) {
if(mouse[0] > width/2)
return "translate("+ (mouse[0]-250) + ", " + (height-10) + ")"
else
return "translate("+ (mouse[0]+10) + ", " + (height-10) + ")"
})
d3.selectAll(".tooltip").attr("transform", function(d, i) {
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(d.values, xDate);
temp_list = []
for(k=0; k< cities.length; k++)
{
temp_list.push( [k, cities[k].values[idx].temperature] );
}
temp_list.sort(function(first, second) {
return second[1] - first[1];
});
// console.log(d.values[idx].temperature)
d3.select(this).text(d.name + " : " + (d.values[idx].temperature).toFixed(2));
d3.selectAll(".tooltip_date").text(d.values[idx].date);
pos = y(temp_list[0][1]);
for(k=0; k< temp_list.length; k++)
{
if(pos > y(temp_list[k][1])-k*20 )
pos += 20;
else
pos = y(temp_list[k][1]);
if(temp_list[k][0] == i)
break;
}

return "translate("+ (mouse[0]+10) + ", " + pos + ")"
})

d3.selectAll(".mouse-per-line")
.attr("transform", function(d, i) {
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(d.values, xDate);
var beginning = 0,
end = lines[i].getTotalLength(),
target = null;
// while (true){
// target = Math.floor((beginning + end) / 2);
// pos = lines[i].getPointAtLength(target);
// if ((target === end || target === beginning) && pos.x !== mouse[0]) {
// break;
// }
// if (pos.x > mouse[0]) end = target;
// else if (pos.x < mouse[0]) beginning = target;
// else break; //position found
// }
return "translate(" + x(xDate) + "," + y(d.values[idx].temperature) +")";
});
});
};
</script>
</html>





라즈베리파이로 지역난방 IOT 만들기 #1 계획 세우기 


라즈베리파이로 지역난방 IOT 만들기 #2 개발


, , , , , , , ,

  1. Blog Icon
    줄돔

    저도 웹서버 + 외부 센서 입력에 따라 특정 동작을 하는 프로젝트를 진행중인데 서버를 띄워놓은 상태에서 다른 동작(예를 들어 하드웨어 스위치)을 어떻게 받아들일 수 있을까요? 서버에는 상시 접속할 수 있는 상황은 아닙니다.

  2. 잘 보고 갑니다...